import SwiftSignalKit import Postbox import TelegramApi public extension TelegramEngine { final class Stickers { private let account: Account init(account: Account) { self.account = account } public func archivedStickerPacks(namespace: ArchivedStickerPacksNamespace = .stickers) -> Signal<[ArchivedStickerPackItem], NoError> { return _internal_archivedStickerPacks(account: account, namespace: namespace) } public func removeArchivedStickerPack(info: StickerPackCollectionInfo) -> Signal { return _internal_removeArchivedStickerPack(account: self.account, info: info) } public func cachedStickerPack(reference: StickerPackReference, forceRemote: Bool) -> Signal { return _internal_cachedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceRemote: forceRemote) } public func loadedStickerPack(reference: StickerPackReference, forceActualized: Bool) -> Signal { return _internal_loadedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceActualized: forceActualized) } public func randomGreetingSticker() -> Signal { return _internal_randomGreetingSticker(account: self.account) } public func searchStickers(query: [String], scope: SearchStickersScope = [.installed, .remote]) -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> { return _internal_searchStickers(account: self.account, query: query, scope: scope) } public func searchStickerSetsRemotely(query: String) -> Signal { return _internal_searchStickerSetsRemotely(network: self.account.network, query: query) } public func searchEmojiSetsRemotely(query: String) -> Signal { return _internal_searchEmojiSetsRemotely(postbox: self.account.postbox, network: self.account.network, query: query) } public func searchStickerSets(query: String) -> Signal { return _internal_searchStickerSets(postbox: self.account.postbox, query: query) } public func searchEmojiSets(query: String) -> Signal { return _internal_searchEmojiSets(postbox: self.account.postbox, query: query) } public func searchGifs(query: String, nextOffset: String = "") -> Signal { return _internal_searchGifs(account: self.account, query: query, nextOffset: nextOffset) } public func addStickerPackInteractively(info: StickerPackCollectionInfo, items: [StickerPackItem], positionInList: Int? = nil) -> Signal { return _internal_addStickerPackInteractively(postbox: self.account.postbox, info: info, items: items, positionInList: positionInList) } public func removeStickerPackInteractively(id: ItemCollectionId, option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> { return _internal_removeStickerPackInteractively(postbox: self.account.postbox, id: id, option: option) } public func removeStickerPacksInteractively(ids: [ItemCollectionId], option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> { return _internal_removeStickerPacksInteractively(postbox: self.account.postbox, ids: ids, option: option) } public func markFeaturedStickerPacksAsSeenInteractively(ids: [ItemCollectionId]) -> Signal { return _internal_markFeaturedStickerPacksAsSeenInteractively(postbox: self.account.postbox, ids: ids) } public func searchEmojiKeywords(inputLanguageCode: String, query: String, completeMatch: Bool) -> Signal<[EmojiKeywordItem], NoError> { return _internal_searchEmojiKeywords(postbox: self.account.postbox, inputLanguageCode: inputLanguageCode, query: query, completeMatch: completeMatch) } public func stickerPacksAttachedToMedia(media: AnyMediaReference) -> Signal<[StickerPackReference], NoError> { return _internal_stickerPacksAttachedToMedia(account: self.account, media: media) } public func uploadSticker(peer: Peer, resource: MediaResource, thumbnail: MediaResource?, alt: String, dimensions: PixelDimensions, duration: Double?, mimeType: String) -> Signal { return _internal_uploadSticker(account: self.account, peer: peer, resource: resource, thumbnail: thumbnail, alt: alt, dimensions: dimensions, duration: duration, mimeType: mimeType) } public func createStickerSet(title: String, shortName: String, stickers: [ImportSticker], thumbnail: ImportSticker?, type: CreateStickerSetType, software: String?) -> Signal { return _internal_createStickerSet(account: self.account, title: title, shortName: shortName, stickers: stickers, thumbnail: thumbnail, type: type, software: software) } public func renameStickerSet(packReference: StickerPackReference, title: String) -> Signal { return _internal_renameStickerSet(account: self.account, packReference: packReference, title: title) } public func deleteStickerSet(packReference: StickerPackReference) -> Signal { return _internal_deleteStickerSet(account: self.account, packReference: packReference) } public func addStickerToStickerSet(packReference: StickerPackReference, sticker: ImportSticker) -> Signal { return _internal_addStickerToStickerSet(account: self.account, packReference: packReference, sticker: sticker) } public func reorderSticker(sticker: FileMediaReference, position: Int) -> Signal { return _internal_reorderSticker(account: self.account, sticker: sticker, position: position) } public func deleteStickerFromStickerSet(sticker: FileMediaReference) -> Signal { return _internal_deleteStickerFromStickerSet(account: self.account, sticker: sticker) } public func replaceSticker(previousSticker: FileMediaReference, sticker: ImportSticker) -> Signal { return _internal_replaceSticker(account: self.account, previousSticker: previousSticker, sticker: sticker) } public func getMyStickerSets() -> Signal<[(StickerPackCollectionInfo, StickerPackItem?)], NoError> { return _internal_getMyStickerSets(account: self.account) } public func getStickerSetShortNameSuggestion(title: String) -> Signal { return _internal_getStickerSetShortNameSuggestion(account: self.account, title: title) } public func toggleStickerSaved(file: TelegramMediaFile, saved: Bool) -> Signal { return _internal_toggleStickerSaved(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId, file: file, saved: saved) } public func validateStickerSetShortNameInteractive(shortName: String) -> Signal { if let error = _internal_checkAddressNameFormat(shortName) { return .single(.invalidFormat(error)) } else { return .single(.checking) |> then( _internal_stickerSetShortNameAvailability(account: self.account, shortName: shortName) |> delay(0.3, queue: Queue.concurrentDefaultQueue()) |> map { result -> AddressNameValidationStatus in .availability(result) } ) } } public func availableReactions() -> Signal { return _internal_cachedAvailableReactions(postbox: self.account.postbox) } public func savedMessageTagData() -> Signal { return self.account.postbox.combinedView(keys: [PostboxViewKey.cachedItem(_internal_savedMessageTagsCacheKey())]) |> mapToSignal { views -> Signal in guard let views = views.views[PostboxViewKey.cachedItem(_internal_savedMessageTagsCacheKey())] as? CachedItemView else { return .single(nil) } guard let savedMessageTags = views.value?.get(SavedMessageTags.self) else { return .single(nil) } return .single(savedMessageTags) } } public func savedMessageTags() -> Signal<([SavedMessageTags.Tag], [Int64: TelegramMediaFile]), NoError> { return self.account.postbox.combinedView(keys: [PostboxViewKey.cachedItem(_internal_savedMessageTagsCacheKey())]) |> mapToSignal { views -> Signal<([SavedMessageTags.Tag], [Int64: TelegramMediaFile]), NoError> in guard let views = views.views[PostboxViewKey.cachedItem(_internal_savedMessageTagsCacheKey())] as? CachedItemView else { return .single(([], [:])) } guard let savedMessageTags = views.value?.get(SavedMessageTags.self) else { return .single(([], [:])) } return self.account.postbox.transaction { transaction -> ([SavedMessageTags.Tag], [Int64: TelegramMediaFile]) in var files: [Int64: TelegramMediaFile] = [:] for tag in savedMessageTags.tags { if case let .custom(fileId) = tag.reaction { let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) if let file = transaction.getMedia(mediaId) as? TelegramMediaFile { files[fileId] = file } } } return (savedMessageTags.tags, files) } } } private var refreshedSavedMessageTags = Atomic>(value: Set()) public func refreshSavedMessageTags(subPeerId: EnginePeer.Id?) -> Signal { var force = false let _ = refreshedSavedMessageTags.modify { value in var value = value if !value.contains(subPeerId) { value.insert(subPeerId) force = true } return value } return synchronizeSavedMessageTags(postbox: self.account.postbox, network: self.account.network, peerId: self.account.peerId, threadId: subPeerId?.toInt64(), force: force) } public func setSavedMessageTagTitle(reaction: MessageReaction.Reaction, title: String?) -> Signal { return _internal_setSavedMessageTagTitle(account: self.account, reaction: reaction, title: title) } public func emojiSearchCategories(kind: EmojiSearchCategories.Kind) -> Signal { return _internal_cachedEmojiSearchCategories(postbox: self.account.postbox, kind: kind) } public func updateQuickReaction(reaction: MessageReaction.Reaction) -> Signal { let _ = updateReactionSettingsInteractively(postbox: self.account.postbox, { settings in var settings = settings settings.quickReaction = reaction return settings }).start() return _internal_updateDefaultReaction(account: self.account, reaction: reaction) } public func isStickerSaved(id: EngineMedia.Id) -> Signal { return self.account.postbox.transaction { transaction -> Bool in return getIsStickerSaved(transaction: transaction, fileId: id) } } public func isGifSaved(id: EngineMedia.Id) -> Signal { return self.account.postbox.transaction { transaction -> Bool in return getIsGifSaved(transaction: transaction, mediaId: id) } } public func clearRecentlyUsedStickers() -> Signal { return self.account.postbox.transaction { transaction -> Void in _internal_clearRecentlyUsedStickers(transaction: transaction) } |> ignoreValues } public func clearRecentlyUsedEmoji() -> Signal { return self.account.postbox.transaction { transaction -> Void in _internal_clearRecentlyUsedEmoji(transaction: transaction) } |> ignoreValues } public func clearRecentlyUsedReactions() -> Signal { let _ = self.account.postbox.transaction({ transaction -> Void in transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudRecentReactions, items: []) }).start() return self.account.network.request(Api.functions.messages.clearRecentReactions()) |> `catch` { _ -> Signal in return .single(.boolFalse) } |> ignoreValues } public func reorderStickerPacks(namespace: ItemCollectionId.Namespace, itemIds: [ItemCollectionId]) -> Signal { return self.account.postbox.transaction { transaction -> Void in let infos = transaction.getItemCollectionsInfos(namespace: namespace) var packDict: [ItemCollectionId: Int] = [:] for i in 0 ..< infos.count { packDict[infos[i].0] = i } var tempSortedPacks: [(ItemCollectionId, ItemCollectionInfo)] = [] var processedPacks = Set() for id in itemIds { if let index = packDict[id] { tempSortedPacks.append(infos[index]) processedPacks.insert(id) } } let restPacks = infos.filter { !processedPacks.contains($0.0) } let sortedPacks = restPacks + tempSortedPacks addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespace, content: .sync, noDelay: false) transaction.replaceItemCollectionInfos(namespace: namespace, itemCollectionInfos: sortedPacks) } |> ignoreValues } public func resolveInlineStickers(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> { return _internal_resolveInlineStickers(postbox: self.account.postbox, network: self.account.network, fileIds: fileIds) } public func resolveInlineStickersLocal(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> { return _internal_resolveInlineStickersLocal(postbox: self.account.postbox, fileIds: fileIds) } public func searchEmoji(emojiString: [String]) -> Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError> { return _internal_searchEmoji(account: self.account, query: emojiString) |> map { items, isFinalResult -> (items: [TelegramMediaFile], isFinalResult: Bool) in return (items.map(\.file), isFinalResult) } } public func addRecentlyUsedSticker(fileReference: FileMediaReference) { let _ = self.account.postbox.transaction({ transaction -> Void in TelegramCore.addRecentlyUsedSticker(transaction: transaction, fileReference: fileReference) }).start() } public func removeRecentlyUsedSticker(fileReference: FileMediaReference) { let _ = self.account.postbox.transaction({ transaction -> Void in _internal_removeRecentlyUsedSticker(transaction: transaction, fileReference: fileReference) }).start() } } } public func _internal_resolveInlineStickers(postbox: Postbox, network: Network, fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> { if fileIds.isEmpty { return .single([:]) } return postbox.transaction { transaction -> [Int64: TelegramMediaFile] in var cachedFiles: [Int64: TelegramMediaFile] = [:] for fileId in fileIds { if let file = transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile { cachedFiles[fileId] = file } } return cachedFiles } |> mapToSignal { cachedFiles -> Signal<[Int64: TelegramMediaFile], NoError> in if cachedFiles.count == fileIds.count { return .single(cachedFiles) } var unknownIds = Set() for fileId in fileIds { if cachedFiles[fileId] == nil { unknownIds.insert(fileId) } } var signals: [Signal<[Api.Document]?, NoError>] = [] var remainingIds = Array(unknownIds) while !remainingIds.isEmpty { let partIdCount = min(100, remainingIds.count) let partIds = remainingIds.prefix(partIdCount) remainingIds.removeFirst(partIdCount) signals.append(network.request(Api.functions.messages.getCustomEmojiDocuments(documentId: Array(partIds))) |> map(Optional.init) |> `catch` { _ -> Signal<[Api.Document]?, NoError> in return .single(nil) }) } return combineLatest(signals) |> mapToSignal { documentSets -> Signal<[Int64: TelegramMediaFile], NoError> in return postbox.transaction { transaction -> [Int64: TelegramMediaFile] in var resultFiles: [Int64: TelegramMediaFile] = cachedFiles for result in documentSets { if let result = result { for document in result { if let file = telegramMediaFileFromApiDocument(document) { resultFiles[file.fileId.id] = file transaction.storeMediaIfNotPresent(media: file) } } } } return resultFiles } } } } func _internal_resolveInlineStickersLocal(postbox: Postbox, fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> { if fileIds.isEmpty { return .single([:]) } return postbox.transaction { transaction -> [Int64: TelegramMediaFile] in var cachedFiles: [Int64: TelegramMediaFile] = [:] for fileId in fileIds { if let file = transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile { cachedFiles[fileId] = file } } return cachedFiles } |> mapToSignal { cachedFiles -> Signal<[Int64: TelegramMediaFile], NoError> in return .single(cachedFiles) } }