Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,65 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
public enum ArchivedStickerPacksNamespace: Int32 {
case stickers = 0
case masks = 1
case emoji = 2
var itemCollectionNamespace: ItemCollectionId.Namespace {
switch self {
case .stickers:
return Namespaces.ItemCollection.CloudStickerPacks
case .masks:
return Namespaces.ItemCollection.CloudMaskPacks
case .emoji:
return Namespaces.ItemCollection.CloudEmojiPacks
}
}
}
public final class ArchivedStickerPackItem {
public let info: StickerPackCollectionInfo
public let topItems: [StickerPackItem]
public init(info: StickerPackCollectionInfo, topItems: [StickerPackItem]) {
self.info = info
self.topItems = topItems
}
}
func _internal_archivedStickerPacks(account: Account, namespace: ArchivedStickerPacksNamespace = .stickers) -> Signal<[ArchivedStickerPackItem], NoError> {
var flags: Int32 = 0
if case .masks = namespace {
flags |= 1 << 0
} else if case .emoji = namespace {
flags |= 1 << 1
}
return account.network.request(Api.functions.messages.getArchivedStickers(flags: flags, offsetId: 0, limit: 200))
|> map { result -> [ArchivedStickerPackItem] in
var archivedItems: [ArchivedStickerPackItem] = []
switch result {
case let .archivedStickers(_, sets):
for set in sets {
let (info, items) = parsePreviewStickerSet(set, namespace: namespace.itemCollectionNamespace)
archivedItems.append(ArchivedStickerPackItem(info: info, topItems: items))
}
}
return archivedItems
} |> `catch` { _ in
return .single([])
}
}
func _internal_removeArchivedStickerPack(account: Account, info: StickerPackCollectionInfo) -> Signal<Void, NoError> {
return account.network.request(Api.functions.messages.uninstallStickerSet(stickerset: Api.InputStickerSet.inputStickerSetID(id: info.id.id, accessHash: info.accessHash)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
}
@@ -0,0 +1,406 @@
import Foundation
import Postbox
import SwiftSignalKit
import MurMurHash32
public enum CachedStickerPackResult {
case none
case fetching
case result(StickerPackCollectionInfo.Accessor, [StickerPackItem], Bool)
}
func cacheStickerPack(transaction: Transaction, info: StickerPackCollectionInfo, items: [StickerPackItem], reference: StickerPackReference? = nil) {
guard let entry = CodableEntry(CachedStickerPack(info: info, items: items, hash: info.hash)) else {
return
}
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(info.id)), entry: entry)
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: info.shortName.lowercased())), entry: entry)
if let reference = reference {
var namespace: Int32?
var id: ItemCollectionId.Id?
switch reference {
case .animatedEmoji:
namespace = Namespaces.ItemCollection.CloudAnimatedEmoji
id = 0
case .animatedEmojiAnimations:
namespace = Namespaces.ItemCollection.CloudAnimatedEmojiAnimations
id = 0
case let .dice(emoji):
namespace = Namespaces.ItemCollection.CloudDice
id = Int64(murMurHashString32(emoji))
case .emojiGenericAnimations:
namespace = Namespaces.ItemCollection.CloudEmojiGenericAnimations
id = 0
case .iconStatusEmoji:
namespace = Namespaces.ItemCollection.CloudIconStatusEmoji
id = 0
case .iconChannelStatusEmoji:
namespace = Namespaces.ItemCollection.CloudIconChannelStatusEmoji
id = 0
case .iconTopicEmoji:
namespace = Namespaces.ItemCollection.CloudIconTopicEmoji
id = 0
case .premiumGifts:
namespace = Namespaces.ItemCollection.CloudPremiumGifts
id = 0
case let .id(_id, _):
if info.flags.contains(.isEmoji) {
namespace = Namespaces.ItemCollection.CloudEmojiPacks
} else {
namespace = Namespaces.ItemCollection.CloudStickerPacks
}
id = _id
case .name:
namespace = info.id.namespace
id = info.id.id
case .tonGifts:
namespace = Namespaces.ItemCollection.CloudTonGifts
id = 0
}
if let namespace = namespace, let id = id {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))), entry: entry)
}
}
}
func _internal_cachedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceRemote: Bool, ignoreCache: Bool = false) -> Signal<CachedStickerPackResult, NoError> {
return postbox.transaction { transaction -> CachedStickerPackResult? in
if let (info, items, local) = cachedStickerPack(transaction: transaction, reference: reference) {
if local && !ignoreCache {
return .result(info, items, true)
}
}
return nil
}
|> mapToSignal { value -> Signal<CachedStickerPackResult, NoError> in
if let value = value {
return .single(value)
} else {
return postbox.transaction { transaction -> (CachedStickerPackResult, Bool, Int32?) in
let namespace = Namespaces.ItemCollection.CloudStickerPacks
var previousHash: Int32?
switch reference {
case let .id(id, _):
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
case let .name(shortName):
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: shortName.lowercased())))?.get(CachedStickerPack.self), let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
case .animatedEmoji:
let namespace = Namespaces.ItemCollection.CloudAnimatedEmoji
let id: ItemCollectionId.Id = 0
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
case let .dice(emoji):
let namespace = Namespaces.ItemCollection.CloudDice
let id: ItemCollectionId.Id = Int64(murMurHashString32(emoji))
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
case .animatedEmojiAnimations:
let namespace = Namespaces.ItemCollection.CloudAnimatedEmojiAnimations
let id: ItemCollectionId.Id = 0
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
case .premiumGifts:
let namespace = Namespaces.ItemCollection.CloudPremiumGifts
let id: ItemCollectionId.Id = 0
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
case .tonGifts:
let namespace = Namespaces.ItemCollection.CloudTonGifts
let id: ItemCollectionId.Id = 0
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
case .emojiGenericAnimations:
let namespace = Namespaces.ItemCollection.CloudEmojiGenericAnimations
let id: ItemCollectionId.Id = 0
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
case .iconStatusEmoji:
let namespace = Namespaces.ItemCollection.CloudIconStatusEmoji
let id: ItemCollectionId.Id = 0
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
case .iconChannelStatusEmoji:
let namespace = Namespaces.ItemCollection.CloudIconChannelStatusEmoji
let id: ItemCollectionId.Id = 0
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
case .iconTopicEmoji:
let namespace = Namespaces.ItemCollection.CloudIconTopicEmoji
let id: ItemCollectionId.Id = 0
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
}
}
|> mapToSignal { result, loadRemote, previousHash in
if loadRemote || forceRemote {
let appliedRemote = updatedRemoteStickerPack(postbox: postbox, network: network, reference: reference)
|> mapToSignal { result -> Signal<CachedStickerPackResult, NoError> in
if let result = result, result.0.hash == previousHash {
return .complete()
}
return postbox.transaction { transaction -> CachedStickerPackResult in
if let result = result {
cacheStickerPack(transaction: transaction, info: result.0, items: result.1, reference: reference)
let currentInfo = transaction.getItemCollectionInfo(collectionId: result.0.id) as? StickerPackCollectionInfo
return .result(StickerPackCollectionInfo.Accessor(result.0), result.1, currentInfo != nil)
} else {
return .none
}
}
}
return .single(result)
|> then(appliedRemote)
} else {
return .single(result)
}
}
}
}
}
func cachedStickerPack(transaction: Transaction, reference: StickerPackReference) -> (StickerPackCollectionInfo.Accessor, [StickerPackItem], Bool)? {
let namespaces: [Int32] = [Namespaces.ItemCollection.CloudStickerPacks, Namespaces.ItemCollection.CloudMaskPacks, Namespaces.ItemCollection.CloudEmojiPacks]
switch reference {
case let .id(id, _):
for namespace in namespaces {
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (StickerPackCollectionInfo.Accessor(currentInfo), items.compactMap { $0 as? StickerPackItem }, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
return (info, cached.items, false)
}
}
case let .name(shortName):
let shortName = shortName.lowercased()
for namespace in namespaces {
for info in transaction.getItemCollectionsInfos(namespace: namespace) {
if let info = info.1 as? StickerPackCollectionInfo {
if info.shortName.lowercased() == shortName {
let items = transaction.getItemCollectionItems(collectionId: info.id)
if !items.isEmpty {
return (StickerPackCollectionInfo.Accessor(info), items.compactMap { $0 as? StickerPackItem }, true)
}
}
}
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: shortName.lowercased())))?.get(CachedStickerPack.self), let info = cached.info {
return (info, cached.items, false)
}
case .animatedEmoji:
let namespace = Namespaces.ItemCollection.CloudAnimatedEmoji
let id: ItemCollectionId.Id = 0
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (StickerPackCollectionInfo.Accessor(currentInfo), items.compactMap { $0 as? StickerPackItem }, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
return (info, cached.items, false)
}
case let .dice(emoji):
let namespace = Namespaces.ItemCollection.CloudDice
let id: ItemCollectionId.Id = Int64(murMurHashString32(emoji))
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (StickerPackCollectionInfo.Accessor(currentInfo), items.compactMap { $0 as? StickerPackItem }, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
return (info, cached.items, false)
}
case .animatedEmojiAnimations:
let namespace = Namespaces.ItemCollection.CloudAnimatedEmojiAnimations
let id: ItemCollectionId.Id = 0
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (StickerPackCollectionInfo.Accessor(currentInfo), items.compactMap { $0 as? StickerPackItem }, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
return (info, cached.items, false)
}
case .premiumGifts:
let namespace = Namespaces.ItemCollection.CloudPremiumGifts
let id: ItemCollectionId.Id = 0
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (StickerPackCollectionInfo.Accessor(currentInfo), items.compactMap { $0 as? StickerPackItem }, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
return (info, cached.items, false)
}
case .tonGifts:
let namespace = Namespaces.ItemCollection.CloudTonGifts
let id: ItemCollectionId.Id = 0
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (StickerPackCollectionInfo.Accessor(currentInfo), items.compactMap { $0 as? StickerPackItem }, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
return (info, cached.items, false)
}
case .emojiGenericAnimations:
let namespace = Namespaces.ItemCollection.CloudEmojiGenericAnimations
let id: ItemCollectionId.Id = 0
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (StickerPackCollectionInfo.Accessor(currentInfo), items.compactMap { $0 as? StickerPackItem }, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
return (info, cached.items, false)
}
case .iconStatusEmoji:
let namespace = Namespaces.ItemCollection.CloudIconStatusEmoji
let id: ItemCollectionId.Id = 0
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (StickerPackCollectionInfo.Accessor(currentInfo), items.compactMap { $0 as? StickerPackItem }, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
return (info, cached.items, false)
}
case .iconChannelStatusEmoji:
let namespace = Namespaces.ItemCollection.CloudIconChannelStatusEmoji
let id: ItemCollectionId.Id = 0
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (StickerPackCollectionInfo.Accessor(currentInfo), items.compactMap { $0 as? StickerPackItem }, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
return (info, cached.items, false)
}
case .iconTopicEmoji:
let namespace = Namespaces.ItemCollection.CloudIconTopicEmoji
let id: ItemCollectionId.Id = 0
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (StickerPackCollectionInfo.Accessor(currentInfo), items.compactMap { $0 as? StickerPackItem }, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))))?.get(CachedStickerPack.self), let info = cached.info {
return (info, cached.items, false)
}
}
return nil
}
@@ -0,0 +1,91 @@
import Foundation
import Postbox
import SwiftSignalKit
private let refreshTimeout: Int32 = 60 * 60
private enum SearchEmojiKeywordsIntermediateResult {
case updating(timestamp: Int32?)
case completed([EmojiKeywordItem])
}
func _internal_searchEmojiKeywords(postbox: Postbox, inputLanguageCode: String, query: String, completeMatch: Bool) -> Signal<[EmojiKeywordItem], NoError> {
guard !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return .single([])
}
let collectionId = emojiKeywordColletionIdForCode(inputLanguageCode)
let search: (Transaction) -> [EmojiKeywordItem] = { transaction in
let queryTokens = stringIndexTokens(query, transliteration: .none)
if let firstQueryToken = queryTokens.first {
let query: ItemCollectionSearchQuery = completeMatch ? .exact(firstQueryToken) : .matching(queryTokens)
let items = transaction.searchItemCollection(namespace: Namespaces.ItemCollection.EmojiKeywords, query: query).filter { item -> Bool in
if let item = item as? EmojiKeywordItem, item.collectionId == collectionId.id {
return true
} else {
return false
}
} as? [EmojiKeywordItem]
if let items = items {
return items.sorted(by: { lhs, rhs -> Bool in
if lhs.keyword.count == rhs.keyword.count {
return lhs.keyword < rhs.keyword
} else {
return lhs.keyword.count < rhs.keyword.count
}
})
}
}
return []
}
return postbox.transaction { transaction -> Signal<SearchEmojiKeywordsIntermediateResult, NoError> in
let currentTime = Int32(CFAbsoluteTimeGetCurrent())
let info = transaction.getItemCollectionInfo(collectionId: collectionId)
if let info = info as? EmojiKeywordCollectionInfo {
if info.timestamp + refreshTimeout < currentTime {
addSynchronizeEmojiKeywordsOperation(transaction: transaction, inputLanguageCode: inputLanguageCode, languageCode: info.languageCode, fromVersion: info.version)
return .single(.updating(timestamp: info.timestamp))
} else {
return .single(.completed(search(transaction)))
}
} else {
addSynchronizeEmojiKeywordsOperation(transaction: transaction, inputLanguageCode: inputLanguageCode, languageCode: nil, fromVersion: nil)
return .single(.updating(timestamp: nil))
}
}
|> switchToLatest
|> mapToSignal { intermediateResult -> Signal<[EmojiKeywordItem], NoError> in
switch intermediateResult {
case let .updating(timestamp):
return postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.EmojiKeywords], aroundIndex: nil, count: 10)
|> filter { view -> Bool in
for info in view.collectionInfos {
if let info = info.1 as? EmojiKeywordCollectionInfo, info.id == collectionId {
if let timestamp = timestamp {
return timestamp < info.timestamp
} else {
return true
}
}
}
return false
}
|> take(1)
|> mapToSignal { view -> Signal<[EmojiKeywordItem], NoError> in
for info in view.collectionInfos {
if let info = info.1 as? EmojiKeywordCollectionInfo, info.id == collectionId {
return postbox.transaction { transaction -> [EmojiKeywordItem] in
return search(transaction)
}
}
}
return .complete()
}
case let .completed(items):
return .single(items)
}
}
}
@@ -0,0 +1,692 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum UploadStickerStatus {
case progress(Float)
case complete(CloudDocumentMediaResource, String)
}
public enum UploadStickerError {
case generic
}
private struct UploadedStickerData {
fileprivate let resource: MediaResource
fileprivate let content: UploadedStickerDataContent
}
private enum UploadedStickerDataContent {
case result(MultipartUploadResult)
case error
}
private func uploadedSticker(postbox: Postbox, network: Network, resource: MediaResource) -> Signal<UploadedStickerData, NoError> {
return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .stickers, userContentType: .sticker), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false)
|> map { result -> UploadedStickerData in
return UploadedStickerData(resource: resource, content: .result(result))
}
|> `catch` { _ -> Signal<UploadedStickerData, NoError> in
return .single(UploadedStickerData(resource: resource, content: .error))
}
}
func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResource, thumbnail: MediaResource? = nil, alt: String, dimensions: PixelDimensions, duration: Double?, mimeType: String) -> Signal<UploadStickerStatus, UploadStickerError> {
guard let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
let uploadSticker = uploadedSticker(postbox: account.postbox, network: account.network, resource: resource)
let uploadThumbnail: Signal<UploadedStickerData?, NoError>
if let thumbnail {
uploadThumbnail = uploadedSticker(postbox: account.postbox, network: account.network, resource: thumbnail)
|> map(Optional.init)
} else {
uploadThumbnail = .single(nil)
}
return combineLatest(uploadSticker, uploadThumbnail)
|> mapError { _ -> UploadStickerError in }
|> mapToSignal { result, thumbnailResult -> Signal<UploadStickerStatus, UploadStickerError> in
switch result.content {
case .error:
return .fail(.generic)
case let .result(resultData):
switch resultData {
case let .progress(progress):
return .single(.progress(progress))
case let .inputFile(file):
var ready = false
var thumbnailFile: Api.InputFile?
if thumbnailResult == nil {
ready = true
} else if let thumbnailResult = thumbnailResult {
if case let .result(thumbnailResultData) = thumbnailResult.content {
if case let .inputFile(file) = thumbnailResultData {
ready = true
thumbnailFile = file
}
} else {
ready = true
}
}
if ready {
var flags: Int32 = 0
if let _ = thumbnailFile {
flags |= (1 << 2)
}
var attributes: [Api.DocumentAttribute] = []
attributes.append(.documentAttributeSticker(flags: 0, alt: alt, stickerset: .inputStickerSetEmpty, maskCoords: nil))
if let duration {
attributes.append(.documentAttributeVideo(flags: 0, duration: duration, w: dimensions.width, h: dimensions.height, preloadPrefixSize: nil, videoStartTs: nil, videoCodec: nil))
}
attributes.append(.documentAttributeImageSize(w: dimensions.width, h: dimensions.height))
return account.network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: thumbnailFile, mimeType: mimeType, attributes: attributes, stickers: nil, videoCover: nil, videoTimestamp: nil, ttlSeconds: nil)))
|> mapError { _ -> UploadStickerError in return .generic }
|> mapToSignal { media -> Signal<UploadStickerStatus, UploadStickerError> in
switch media {
case let .messageMediaDocument(_, document, altDocuments, _, _, _):
if let document = document, let file = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments), let uploadedResource = file.resource as? CloudDocumentMediaResource {
account.postbox.mediaBox.copyResourceData(from: resource.id, to: uploadedResource.id, synchronous: true)
if let thumbnail, let previewRepresentation = file.previewRepresentations.first(where: { $0.dimensions == PixelDimensions(width: 320, height: 320) }) {
account.postbox.mediaBox.copyResourceData(from: thumbnail.id, to: previewRepresentation.resource.id, synchronous: true)
}
return .single(.complete(uploadedResource, file.mimeType))
}
default:
break
}
return .fail(.generic)
}
} else {
return .single(.progress(1.0))
}
default:
return .fail(.generic)
}
}
}
}
public enum CreateStickerSetError {
case generic
}
public struct ImportSticker {
public let resource: MediaResourceReference
public let thumbnailResource: MediaResourceReference?
let emojis: [String]
public let dimensions: PixelDimensions
public let duration: Double?
public let mimeType: String
public let keywords: String
public init(resource: MediaResourceReference, thumbnailResource: MediaResourceReference? = nil, emojis: [String], dimensions: PixelDimensions, duration: Double?, mimeType: String, keywords: String) {
self.resource = resource
self.thumbnailResource = thumbnailResource
self.emojis = emojis
self.dimensions = dimensions
self.duration = duration
self.mimeType = mimeType
self.keywords = keywords
}
}
public extension ImportSticker {
var stickerPackItem: StickerPackItem? {
guard let resource = self.resource.resource as? TelegramMediaResource else {
return nil
}
var fileAttributes: [TelegramMediaFileAttribute] = []
if self.mimeType == "video/webm" {
fileAttributes.append(.FileName(fileName: "sticker.webm"))
fileAttributes.append(.Animated)
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
fileAttributes.append(.Video(duration: self.duration ?? 3.0, size: self.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil))
} else if self.mimeType == "application/x-tgsticker" {
fileAttributes.append(.FileName(fileName: "sticker.tgs"))
fileAttributes.append(.Animated)
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
} else {
fileAttributes.append(.FileName(fileName: "sticker.webp"))
}
fileAttributes.append(.ImageSize(size: self.dimensions))
var previewRepresentations: [TelegramMediaImageRepresentation] = []
if let thumbnailResource = self.thumbnailResource?.resource as? TelegramMediaResource {
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil))
}
return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: self.mimeType, size: nil, attributes: fileAttributes, alternativeRepresentations: []), indexKeys: [])
}
}
public enum CreateStickerSetStatus {
case progress(Float, Int32, Int32)
case complete(StickerPackCollectionInfo, [StickerPackItem])
}
public enum CreateStickerSetType {
public enum ContentType {
case image
case animation
case video
}
case stickers(content: ContentType)
case emoji(content: ContentType, textColored: Bool)
var contentType: ContentType {
switch self {
case let .stickers(content), let .emoji(content, _):
return content
}
}
}
func _internal_createStickerSet(account: Account, title: String, shortName: String, stickers: [ImportSticker], thumbnail: ImportSticker?, type: CreateStickerSetType, software: String?) -> Signal<CreateStickerSetStatus, CreateStickerSetError> {
return account.postbox.loadedPeerWithId(account.peerId)
|> castError(CreateStickerSetError.self)
|> mapToSignal { peer -> Signal<CreateStickerSetStatus, CreateStickerSetError> in
guard let inputUser = apiInputUser(peer) else {
return .fail(.generic)
}
var uploadStickers: [Signal<UploadStickerStatus, CreateStickerSetError>] = []
var stickers = stickers
if let thumbnail = thumbnail {
stickers.append(thumbnail)
}
for sticker in stickers {
if let resource = sticker.resource.resource as? CloudDocumentMediaResource {
uploadStickers.append(.single(.complete(resource, sticker.mimeType)))
} else {
uploadStickers.append(_internal_uploadSticker(account: account, peer: peer, resource: sticker.resource.resource, thumbnail: sticker.thumbnailResource?.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, duration: sticker.duration, mimeType: sticker.mimeType)
|> mapError { _ -> CreateStickerSetError in
return .generic
})
}
}
return combineLatest(uploadStickers)
|> mapToSignal { uploadedStickers -> Signal<CreateStickerSetStatus, CreateStickerSetError> in
var resources: [CloudDocumentMediaResource] = []
for sticker in uploadedStickers {
if case let .complete(resource, _) = sticker {
resources.append(resource)
}
}
if resources.count == stickers.count {
var flags: Int32 = 0
switch type.contentType {
case .animation:
flags |= (1 << 1)
case .video:
flags |= (1 << 4)
default:
break
}
if case let .emoji(_, textColored) = type {
flags |= (1 << 5)
if textColored {
flags |= (1 << 6)
}
}
var inputStickers: [Api.InputStickerSetItem] = []
let stickerDocuments = thumbnail != nil ? resources.dropLast() : resources
for i in 0 ..< stickerDocuments.count {
let sticker = stickers[i]
let resource = resources[i]
var flags: Int32 = 0
if sticker.keywords.count > 0 {
flags |= (1 << 1)
}
inputStickers.append(.inputStickerSetItem(flags: flags, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), emoji: sticker.emojis.joined(), maskCoords: nil, keywords: sticker.keywords))
}
var thumbnailDocument: Api.InputDocument?
if thumbnail != nil, let resource = resources.last {
flags |= (1 << 2)
thumbnailDocument = .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data()))
}
if let software = software, !software.isEmpty {
flags |= (1 << 3)
}
return account.network.request(Api.functions.stickers.createStickerSet(flags: flags, userId: inputUser, title: title, shortName: shortName, thumb: thumbnailDocument, stickers: inputStickers, software: software))
|> mapError { error -> CreateStickerSetError in
return .generic
}
|> mapToSignal { result -> Signal<CreateStickerSetStatus, CreateStickerSetError> in
guard let (info, items) = parseStickerSetInfoAndItems(apiStickerSet: result) else {
return .complete()
}
return .single(.complete(info, items))
}
} else {
var totalProgress: Float = 0.0
var completeCount: Int32 = 0
for sticker in uploadedStickers {
switch sticker {
case .complete:
totalProgress += 1.0
completeCount += 1
case let .progress(progress):
totalProgress += progress
if progress == 1.0 {
completeCount += 1
}
}
}
let normalizedProgress = min(1.0, max(0.0, totalProgress / Float(stickers.count)))
return .single(.progress(normalizedProgress, completeCount, Int32(uploadedStickers.count)))
}
}
}
}
public enum RenameStickerSetError {
case generic
}
func _internal_renameStickerSet(account: Account, packReference: StickerPackReference, title: String) -> Signal<Never, RenameStickerSetError> {
return account.network.request(Api.functions.stickers.renameStickerSet(stickerset: packReference.apiInputStickerSet, title: title))
|> mapError { error -> RenameStickerSetError in
return .generic
}
|> mapToSignal { result -> Signal<Never, RenameStickerSetError> in
guard let (info, items) = parseStickerSetInfoAndItems(apiStickerSet: result) else {
return .complete()
}
return account.postbox.transaction { transaction -> Void in
let collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks
var currentInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
if let index = currentInfos.firstIndex(where: { $0.id == info.id }) {
currentInfos[index] = info
}
transaction.replaceItemCollectionInfos(namespace: collectionNamespace, itemCollectionInfos: currentInfos.map { ($0.id, $0) })
cacheStickerPack(transaction: transaction, info: info, items: items)
}
|> castError(RenameStickerSetError.self)
|> ignoreValues
}
}
public enum DeleteStickerSetError {
case generic
}
func _internal_deleteStickerSet(account: Account, packReference: StickerPackReference) -> Signal<Never, DeleteStickerSetError> {
return account.network.request(Api.functions.stickers.deleteStickerSet(stickerset: packReference.apiInputStickerSet))
|> mapError { error -> DeleteStickerSetError in
return .generic
}
|> mapToSignal { _ in
return account.postbox.transaction { transaction in
if let (info, _, _) = cachedStickerPack(transaction: transaction, reference: packReference) {
transaction.removeItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(info.id)))
transaction.removeItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: info.shortName.lowercased())))
}
if case let .id(id, _) = packReference {
transaction.removeItemCollection(collectionId: ItemCollectionId(namespace: Namespaces.ItemCollection.CloudStickerPacks, id: id))
}
}
|> castError(DeleteStickerSetError.self)
}
|> ignoreValues
}
public enum AddStickerToSetError {
case generic
}
private func revalidatedSticker<T>(account: Account, sticker: FileMediaReference, signal: @escaping (CloudDocumentMediaResource) -> Signal<T, MTRpcError>) -> Signal<T, MTRpcError> {
guard let resource = sticker.media.resource as? CloudDocumentMediaResource else {
return .fail(MTRpcError(errorCode: 500, errorDescription: "Internal"))
}
return signal(resource)
|> `catch` { error -> Signal<T, MTRpcError> in
if error.errorDescription == "FILE_REFERENCE_EXPIRED" {
return revalidateMediaResourceReference(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, revalidationContext: account.mediaReferenceRevalidationContext, info: TelegramCloudMediaResourceFetchInfo(reference: sticker.resourceReference(resource), preferBackgroundReferenceRevalidation: false, continueInBackground: false), resource: resource)
|> mapError { _ -> MTRpcError in
return MTRpcError(errorCode: 500, errorDescription: "Internal")
}
|> mapToSignal { result -> Signal<T, MTRpcError> in
guard let resource = result.updatedResource as? CloudDocumentMediaResource else {
return .fail(MTRpcError(errorCode: 500, errorDescription: "Internal"))
}
return signal(resource)
}
} else {
return .fail(error)
}
}
}
func _internal_addStickerToStickerSet(account: Account, packReference: StickerPackReference, sticker: ImportSticker) -> Signal<Bool, AddStickerToSetError> {
let uploadSticker: Signal<UploadStickerStatus, AddStickerToSetError>
if let resource = sticker.resource.resource as? CloudDocumentMediaResource {
uploadSticker = .single(.complete(resource, sticker.mimeType))
} else {
uploadSticker = account.postbox.loadedPeerWithId(account.peerId)
|> castError(AddStickerToSetError.self)
|> mapToSignal { peer in
return _internal_uploadSticker(account: account, peer: peer, resource: sticker.resource.resource, thumbnail: sticker.thumbnailResource?.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, duration: sticker.duration, mimeType: sticker.mimeType)
|> mapError { _ -> AddStickerToSetError in
return .generic
}
}
}
return uploadSticker
|> mapToSignal { uploadedSticker in
guard case let .complete(resource, _) = uploadedSticker else {
return .complete()
}
var flags: Int32 = 0
if sticker.keywords.count > 0 {
flags |= (1 << 1)
}
let inputSticker: Api.InputStickerSetItem = .inputStickerSetItem(flags: flags, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), emoji: sticker.emojis.joined(), maskCoords: nil, keywords: sticker.keywords)
return account.network.request(Api.functions.stickers.addStickerToSet(stickerset: packReference.apiInputStickerSet, sticker: inputSticker))
|> `catch` { error -> Signal<Api.messages.StickerSet, MTRpcError> in
if error.errorDescription == "FILE_REFERENCE_EXPIRED" {
return revalidateMediaResourceReference(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, revalidationContext: account.mediaReferenceRevalidationContext, info: TelegramCloudMediaResourceFetchInfo(reference: sticker.resource, preferBackgroundReferenceRevalidation: false, continueInBackground: false), resource: sticker.resource.resource)
|> mapError { _ -> MTRpcError in
return MTRpcError(errorCode: 500, errorDescription: "Internal")
}
|> mapToSignal { result -> Signal<Api.messages.StickerSet, MTRpcError> in
guard let resource = result.updatedResource as? CloudDocumentMediaResource else {
return .fail(MTRpcError(errorCode: 500, errorDescription: "Internal"))
}
let inputSticker: Api.InputStickerSetItem = .inputStickerSetItem(flags: flags, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), emoji: sticker.emojis.joined(), maskCoords: nil, keywords: sticker.keywords)
return account.network.request(Api.functions.stickers.addStickerToSet(stickerset: packReference.apiInputStickerSet, sticker: inputSticker))
}
} else {
return .fail(error)
}
}
|> mapError { error -> AddStickerToSetError in
return .generic
}
|> mapToSignal { result -> Signal<Bool, AddStickerToSetError> in
guard let (info, items) = parseStickerSetInfoAndItems(apiStickerSet: result) else {
return .complete()
}
return account.postbox.transaction { transaction -> Bool in
if transaction.getItemCollectionInfo(collectionId: info.id) != nil {
transaction.replaceItemCollectionItems(collectionId: info.id, items: items)
}
cacheStickerPack(transaction: transaction, info: info, items: items)
return true
}
|> castError(AddStickerToSetError.self)
}
}
}
public enum ReorderStickerError {
case generic
}
func _internal_reorderSticker(account: Account, sticker: FileMediaReference, position: Int) -> Signal<Never, ReorderStickerError> {
return revalidatedSticker(account: account, sticker: sticker, signal: { resource in
return account.network.request(Api.functions.stickers.changeStickerPosition(sticker: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), position: Int32(position)))
})
|> mapError { error -> ReorderStickerError in
return .generic
}
|> mapToSignal { result -> Signal<Never, ReorderStickerError> in
guard let (info, items) = parseStickerSetInfoAndItems(apiStickerSet: result) else {
return .complete()
}
return account.postbox.transaction { transaction -> Void in
if transaction.getItemCollectionInfo(collectionId: info.id) != nil {
transaction.replaceItemCollectionItems(collectionId: info.id, items: items)
}
cacheStickerPack(transaction: transaction, info: info, items: items)
}
|> castError(ReorderStickerError.self)
|> ignoreValues
}
}
public enum DeleteStickerError {
case generic
}
func _internal_deleteStickerFromStickerSet(account: Account, sticker: FileMediaReference) -> Signal<Never, DeleteStickerError> {
return revalidatedSticker(account: account, sticker: sticker, signal: { resource in
return account.network.request(Api.functions.stickers.removeStickerFromSet(sticker: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference))))
})
|> mapError { error -> DeleteStickerError in
return .generic
}
|> mapToSignal { result -> Signal<Never, DeleteStickerError> in
guard let (info, items) = parseStickerSetInfoAndItems(apiStickerSet: result) else {
return .complete()
}
return account.postbox.transaction { transaction -> Void in
if transaction.getItemCollectionInfo(collectionId: info.id) != nil {
transaction.replaceItemCollectionItems(collectionId: info.id, items: items)
}
cacheStickerPack(transaction: transaction, info: info, items: items)
}
|> castError(DeleteStickerError.self)
|> ignoreValues
}
}
public enum ReplaceStickerError {
case generic
}
func _internal_replaceSticker(account: Account, previousSticker: FileMediaReference, sticker: ImportSticker) -> Signal<Never, ReplaceStickerError> {
let uploadSticker: Signal<UploadStickerStatus, ReplaceStickerError>
if let resource = sticker.resource.resource as? CloudDocumentMediaResource {
uploadSticker = .single(.complete(resource, sticker.mimeType))
} else {
uploadSticker = account.postbox.loadedPeerWithId(account.peerId)
|> castError(ReplaceStickerError.self)
|> mapToSignal { peer in
return _internal_uploadSticker(account: account, peer: peer, resource: sticker.resource.resource, thumbnail: sticker.thumbnailResource?.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, duration: sticker.duration, mimeType: sticker.mimeType)
|> mapError { _ -> ReplaceStickerError in
return .generic
}
}
}
return uploadSticker
|> mapToSignal { uploadedSticker in
guard case let .complete(resource, _) = uploadedSticker else {
return .complete()
}
var flags: Int32 = 0
if sticker.keywords.count > 0 {
flags |= (1 << 1)
}
let inputSticker: Api.InputStickerSetItem = .inputStickerSetItem(flags: flags, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), emoji: sticker.emojis.joined(), maskCoords: nil, keywords: sticker.keywords)
return revalidatedSticker(account: account, sticker: previousSticker, signal: { previousResource in
return account.network.request(Api.functions.stickers.replaceSticker(sticker: .inputDocument(id: previousResource.fileId, accessHash: previousResource.accessHash, fileReference: Buffer(data: previousResource.fileReference)), newSticker: inputSticker))
})
|> mapError { error -> ReplaceStickerError in
return .generic
}
|> mapToSignal { result -> Signal<Never, ReplaceStickerError> in
guard let (info, items) = parseStickerSetInfoAndItems(apiStickerSet: result) else {
return .complete()
}
return account.postbox.transaction { transaction -> Void in
if transaction.getItemCollectionInfo(collectionId: info.id) != nil {
transaction.replaceItemCollectionItems(collectionId: info.id, items: items)
}
cacheStickerPack(transaction: transaction, info: info, items: items)
}
|> castError(ReplaceStickerError.self)
|> ignoreValues
}
}
}
func _internal_getMyStickerSets(account: Account) -> Signal<[(StickerPackCollectionInfo, StickerPackItem?)], NoError> {
return account.network.request(Api.functions.messages.getMyStickers(offsetId: 0, limit: 100))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.MyStickers?, NoError> in
return .single(nil)
}
|> map { result -> [(StickerPackCollectionInfo, StickerPackItem?)] in
guard let result else {
return []
}
var infos: [(StickerPackCollectionInfo, StickerPackItem?)] = []
switch result {
case let .myStickers(_, sets):
for set in sets {
switch set {
case let .stickerSetCovered(set, cover):
let namespace: ItemCollectionId.Namespace
switch set {
case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _, _):
if (flags & (1 << 3)) != 0 {
namespace = Namespaces.ItemCollection.CloudMaskPacks
} else if (flags & (1 << 7)) != 0 {
namespace = Namespaces.ItemCollection.CloudEmojiPacks
} else {
namespace = Namespaces.ItemCollection.CloudStickerPacks
}
}
let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var firstItem: StickerPackItem?
if let file = telegramMediaFileFromApiDocument(cover, altDocuments: []), let id = file.id {
firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: [])
}
infos.append((info, firstItem))
case let .stickerSetFullCovered(set, _, _, documents):
let namespace: ItemCollectionId.Namespace
switch set {
case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _, _):
if (flags & (1 << 3)) != 0 {
namespace = Namespaces.ItemCollection.CloudMaskPacks
} else if (flags & (1 << 7)) != 0 {
namespace = Namespaces.ItemCollection.CloudEmojiPacks
} else {
namespace = Namespaces.ItemCollection.CloudStickerPacks
}
}
let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var firstItem: StickerPackItem?
if let apiDocument = documents.first {
if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id {
firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: [])
}
}
infos.append((info, firstItem))
default:
break
}
}
}
return infos
}
}
private func parseStickerSetInfoAndItems(apiStickerSet: Api.messages.StickerSet) -> (StickerPackCollectionInfo, [StickerPackItem])? {
switch apiStickerSet {
case .stickerSetNotModified:
return nil
case let .stickerSet(set, packs, keywords, documents):
let namespace: ItemCollectionId.Namespace
switch set {
case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _, _):
if (flags & (1 << 3)) != 0 {
namespace = Namespaces.ItemCollection.CloudMaskPacks
} else if (flags & (1 << 7)) != 0 {
namespace = Namespaces.ItemCollection.CloudEmojiPacks
} else {
namespace = Namespaces.ItemCollection.CloudStickerPacks
}
}
let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
for pack in packs {
switch pack {
case let .stickerPack(text, fileIds):
let key = ValueBoxKey(text).toMemoryBuffer()
for fileId in fileIds {
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
for keyword in keywords {
switch keyword {
case let .stickerKeyword(documentId, texts):
for text in texts {
let key = ValueBoxKey(text).toMemoryBuffer()
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
var items: [StickerPackItem] = []
for apiDocument in documents {
if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id {
let fileIndexKeys: [MemoryBuffer]
if let indexKeys = indexKeysByFile[id] {
fileIndexKeys = indexKeys
} else {
fileIndexKeys = []
}
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: fileIndexKeys))
}
}
return (info, items)
}
}
func _internal_getStickerSetShortNameSuggestion(account: Account, title: String) -> Signal<String?, NoError> {
return account.network.request(Api.functions.stickers.suggestShortName(title: title))
|> map (Optional.init)
|> `catch` { _ in
return .single(nil)
}
|> map { result in
guard let result = result else {
return nil
}
switch result {
case let .suggestedShortName(shortName):
return shortName
}
}
}
func _internal_stickerSetShortNameAvailability(account: Account, shortName: String) -> Signal<AddressNameAvailability, NoError> {
return account.network.request(Api.functions.stickers.checkShortName(shortName: shortName))
|> map { result -> AddressNameAvailability in
switch result {
case .boolTrue:
return .available
case .boolFalse:
return .taken
}
}
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
if error.errorDescription == "SHORT_NAME_OCCUPIED" {
return .single(.taken)
}
return .single(.invalid)
}
}
@@ -0,0 +1,5 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
@@ -0,0 +1,140 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
extension StickerPackReference {
init(_ stickerPackInfo: StickerPackCollectionInfo) {
self = .id(id: stickerPackInfo.id.id, accessHash: stickerPackInfo.accessHash)
}
var apiInputStickerSet: Api.InputStickerSet {
switch self {
case let .id(id, accessHash):
return .inputStickerSetID(id: id, accessHash: accessHash)
case let .name(name):
return .inputStickerSetShortName(shortName: name)
case .animatedEmoji:
return .inputStickerSetAnimatedEmoji
case let .dice(emoji):
return .inputStickerSetDice(emoticon: emoji)
case .animatedEmojiAnimations:
return .inputStickerSetAnimatedEmojiAnimations
case .premiumGifts:
return .inputStickerSetPremiumGifts
case .emojiGenericAnimations:
return .inputStickerSetEmojiGenericAnimations
case .iconStatusEmoji:
return .inputStickerSetEmojiDefaultStatuses
case .iconChannelStatusEmoji:
return .inputStickerSetEmojiChannelDefaultStatuses
case .iconTopicEmoji:
return .inputStickerSetEmojiDefaultTopicIcons
case .tonGifts:
return .inputStickerSetTonGifts
}
}
}
public enum LoadedStickerPack {
case fetching
case none
case result(info: StickerPackCollectionInfo.Accessor, items: [StickerPackItem], installed: Bool)
}
func updatedRemoteStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference) -> Signal<(StickerPackCollectionInfo, [StickerPackItem])?, NoError> {
return network.request(Api.functions.messages.getStickerSet(stickerset: reference.apiInputStickerSet, hash: 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.StickerSet?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<(StickerPackCollectionInfo, [StickerPackItem])?, NoError> in
guard let result = result else {
return .single(nil)
}
let info: StickerPackCollectionInfo
var items: [StickerPackItem] = []
switch result {
case .stickerSetNotModified:
return .complete()
case let .stickerSet(set, packs, keywords, documents):
let namespace: ItemCollectionId.Namespace
switch set {
case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _, _):
if (flags & (1 << 3)) != 0 {
namespace = Namespaces.ItemCollection.CloudMaskPacks
} else if (flags & (1 << 7)) != 0 {
namespace = Namespaces.ItemCollection.CloudEmojiPacks
} else {
namespace = Namespaces.ItemCollection.CloudStickerPacks
}
}
info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
for pack in packs {
switch pack {
case let .stickerPack(text, fileIds):
let key = ValueBoxKey(text).toMemoryBuffer()
for fileId in fileIds {
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
for keyword in keywords {
switch keyword {
case let .stickerKeyword(documentId, texts):
for text in texts {
let key = ValueBoxKey(text).toMemoryBuffer()
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
for apiDocument in documents {
if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id {
let fileIndexKeys: [MemoryBuffer]
if let indexKeys = indexKeysByFile[id] {
fileIndexKeys = indexKeys
} else {
fileIndexKeys = []
}
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: fileIndexKeys))
}
}
}
return postbox.transaction { transaction -> (StickerPackCollectionInfo, [StickerPackItem])? in
if transaction.getItemCollectionInfo(collectionId: info.id) != nil {
transaction.replaceItemCollectionItems(collectionId: info.id, items: items)
}
cacheStickerPack(transaction: transaction, info: info, items: items)
return (info, items)
}
}
}
func _internal_loadedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceActualized: Bool, ignoreCache: Bool = false) -> Signal<LoadedStickerPack, NoError> {
return _internal_cachedStickerPack(postbox: postbox, network: network, reference: reference, forceRemote: forceActualized)
|> map { result -> LoadedStickerPack in
switch result {
case .none:
return .none
case .fetching:
return .fetching
case let .result(info, items, installed):
return .result(info: info, items: items, installed: installed)
}
}
}
@@ -0,0 +1,50 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
public enum SavedStickerResult {
case generic
case limitExceeded(Int32, Int32)
}
func _internal_toggleStickerSaved(postbox: Postbox, network: Network, accountPeerId: PeerId, file: TelegramMediaFile, saved: Bool) -> Signal<SavedStickerResult, AddSavedStickerError> {
if saved {
return postbox.transaction { transaction -> Signal<SavedStickerResult, AddSavedStickerError> in
let isPremium = transaction.getPeer(accountPeerId)?.isPremium ?? false
let items = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudSavedStickers)
let appConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue
let limitsConfiguration = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false)
let premiumLimitsConfiguration = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true)
let result: SavedStickerResult
if isPremium && items.count >= premiumLimitsConfiguration.maxFavedStickerCount {
result = .limitExceeded(premiumLimitsConfiguration.maxFavedStickerCount, premiumLimitsConfiguration.maxFavedStickerCount)
} else if !isPremium && items.count >= limitsConfiguration.maxFavedStickerCount {
result = .limitExceeded(limitsConfiguration.maxFavedStickerCount, premiumLimitsConfiguration.maxFavedStickerCount)
} else {
result = .generic
}
return addSavedSticker(postbox: postbox, network: network, file: file, limit: Int(isPremium ? premiumLimitsConfiguration.maxFavedStickerCount : limitsConfiguration.maxFavedStickerCount))
|> map { _ -> SavedStickerResult in
return .generic
}
|> filter { _ in
return false
}
|> then(
.single(result)
)
}
|> castError(AddSavedStickerError.self)
|> switchToLatest
} else {
return removeSavedSticker(postbox: postbox, mediaId: file.fileId)
|> map { _ -> SavedStickerResult in
return .generic
}
|> castError(AddSavedStickerError.self)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,131 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
import MtProtoKit
func telegramStickerPackThumbnailRepresentationFromApiSizes(datacenterId: Int32, thumbVersion: Int32?, sizes: [Api.PhotoSize]) -> (immediateThumbnail: Data?, representations: [TelegramMediaImageRepresentation]) {
func stickerTypeHint(for type: String) -> TelegramMediaImageRepresentation.TypeHint {
switch type {
case "s":
return .generic
case "a":
return .animated
case "v":
return .video
default:
return .generic
}
}
var immediateThumbnailData: Data?
var representations: [TelegramMediaImageRepresentation] = []
for size in sizes {
switch size {
case let .photoCachedSize(type, w, h, _):
let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, thumbVersion: thumbVersion, volumeId: nil, localId: nil)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, typeHint: stickerTypeHint(for: type)))
case let .photoSize(type, w, h, _):
let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, thumbVersion: thumbVersion, volumeId: nil, localId: nil)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, typeHint: stickerTypeHint(for: type)))
case let .photoSizeProgressive(type, w, h, sizes):
let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, thumbVersion: thumbVersion, volumeId: nil, localId: nil)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes, immediateThumbnailData: nil, typeHint: stickerTypeHint(for: type)))
case let .photoPathSize(_, data):
immediateThumbnailData = data.makeData()
case .photoStrippedSize:
break
case .photoSizeEmpty:
break
}
}
return (immediateThumbnailData, representations)
}
extension StickerPackCollectionInfo {
convenience init(apiSet: Api.StickerSet, namespace: ItemCollectionId.Namespace) {
switch apiSet {
case let .stickerSet(flags, _, id, accessHash, title, shortName, thumbs, thumbDcId, thumbVersion, thumbDocumentId, count, nHash):
var setFlags: StickerPackCollectionInfoFlags = StickerPackCollectionInfoFlags()
if (flags & (1 << 2)) != 0 {
setFlags.insert(.isOfficial)
}
if (flags & (1 << 3)) != 0 {
setFlags.insert(.isMasks)
}
if (flags & (1 << 7)) != 0 {
setFlags.insert(.isEmoji)
}
if (flags & (1 << 9)) != 0 {
setFlags.insert(.isCustomTemplateEmoji)
}
if (flags & (1 << 10)) != 0 {
setFlags.insert(.isAvailableAsChannelStatus)
}
if (flags & (1 << 11)) != 0 {
setFlags.insert(.isCreator)
}
var thumbnailRepresentation: TelegramMediaImageRepresentation?
var immediateThumbnailData: Data?
if let thumbs = thumbs, let thumbDcId = thumbDcId {
let (data, representations) = telegramStickerPackThumbnailRepresentationFromApiSizes(datacenterId: thumbDcId, thumbVersion: thumbVersion, sizes: thumbs)
thumbnailRepresentation = representations.first
immediateThumbnailData = data
}
self.init(id: ItemCollectionId(namespace: namespace, id: id), flags: setFlags, accessHash: accessHash, title: title, shortName: shortName, thumbnail: thumbnailRepresentation, thumbnailFileId: thumbDocumentId, immediateThumbnailData: immediateThumbnailData, hash: nHash, count: count)
}
}
}
func _internal_stickerPacksAttachedToMedia(account: Account, media: AnyMediaReference) -> Signal<[StickerPackReference], NoError> {
let inputMedia: Api.InputStickeredMedia
let resourceReference: MediaResourceReference
if let imageReference = media.concrete(TelegramMediaImage.self), let reference = imageReference.media.reference, case let .cloud(imageId, accessHash, fileReference) = reference, let representation = largestImageRepresentation(imageReference.media.representations) {
inputMedia = .inputStickeredMediaPhoto(id: Api.InputPhoto.inputPhoto(id: imageId, accessHash: accessHash, fileReference: Buffer(data: fileReference ?? Data())))
resourceReference = imageReference.resourceReference(representation.resource)
} else if let fileReference = media.concrete(TelegramMediaFile.self), let resource = fileReference.media.resource as? CloudDocumentMediaResource {
inputMedia = .inputStickeredMediaDocument(id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())))
resourceReference = fileReference.resourceReference(fileReference.media.resource)
} else {
return .single([])
}
let accountPeerId = account.peerId
return account.network.request(Api.functions.messages.getAttachedStickers(media: inputMedia))
|> `catch` { _ -> Signal<[Api.StickerSetCovered], MTRpcError> in
return revalidateMediaResourceReference(accountPeerId: accountPeerId, postbox: account.postbox, network: account.network, revalidationContext: account.mediaReferenceRevalidationContext, info: TelegramCloudMediaResourceFetchInfo(reference: resourceReference, preferBackgroundReferenceRevalidation: false, continueInBackground: false), resource: resourceReference.resource)
|> mapError { _ -> MTRpcError in
return MTRpcError(errorCode: 500, errorDescription: "Internal")
}
|> mapToSignal { reference -> Signal<[Api.StickerSetCovered], MTRpcError> in
let inputMedia: Api.InputStickeredMedia
if let resource = reference.updatedResource as? TelegramCloudMediaResourceWithFileReference, let updatedReference = resource.fileReference {
if let imageReference = media.concrete(TelegramMediaImage.self), let reference = imageReference.media.reference, case let .cloud(imageId, accessHash, _) = reference, let _ = largestImageRepresentation(imageReference.media.representations) {
inputMedia = .inputStickeredMediaPhoto(id: Api.InputPhoto.inputPhoto(id: imageId, accessHash: accessHash, fileReference: Buffer(data: updatedReference)))
} else if let fileReference = media.concrete(TelegramMediaFile.self), let resource = fileReference.media.resource as? CloudDocumentMediaResource {
inputMedia = .inputStickeredMediaDocument(id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: updatedReference)))
} else {
return .single([])
}
return account.network.request(Api.functions.messages.getAttachedStickers(media: inputMedia))
} else {
return .single([])
}
}
|> `catch` { _ -> Signal<[Api.StickerSetCovered], MTRpcError> in
return .single([])
}
}
|> map { result -> [StickerPackReference] in
return result.map { pack in
switch pack {
case let .stickerSetCovered(set, _), let .stickerSetMultiCovered(set, _), let .stickerSetFullCovered(set, _, _, _), let .stickerSetNoCovered(set):
let info = StickerPackCollectionInfo(apiSet: set, namespace: Namespaces.ItemCollection.CloudStickerPacks)
return .id(id: info.id.id, accessHash: info.accessHash)
}
}
}
|> `catch` { _ -> Signal<[StickerPackReference], NoError> in
return .single([])
}
}
@@ -0,0 +1,124 @@
import Foundation
import Postbox
import SwiftSignalKit
func _internal_addStickerPackInteractively(postbox: Postbox, info: StickerPackCollectionInfo, items: [StickerPackItem], positionInList: Int? = nil) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
let namespace: SynchronizeInstalledStickerPacksOperationNamespace?
switch info.id.namespace {
case Namespaces.ItemCollection.CloudStickerPacks:
namespace = .stickers
case Namespaces.ItemCollection.CloudMaskPacks:
namespace = .masks
case Namespaces.ItemCollection.CloudEmojiPacks:
namespace = .emoji
default:
namespace = nil
}
if let namespace = namespace {
var mappedInfo = info
if items.isEmpty {
mappedInfo = StickerPackCollectionInfo(id: info.id, flags: info.flags, accessHash: info.accessHash, title: info.title, shortName: info.shortName, thumbnail: info.thumbnail, thumbnailFileId: info.thumbnailFileId, immediateThumbnailData: info.immediateThumbnailData, hash: Int32(bitPattern: arc4random()), count: info.count)
}
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespace, content: .add([mappedInfo.id]), noDelay: items.isEmpty)
var updatedInfos = transaction.getItemCollectionsInfos(namespace: mappedInfo.id.namespace).map { $0.1 as! StickerPackCollectionInfo }
if let index = updatedInfos.firstIndex(where: { $0.id == mappedInfo.id }) {
let currentInfo = updatedInfos[index]
updatedInfos.remove(at: index)
updatedInfos.insert(currentInfo, at: 0)
} else {
if let positionInList = positionInList, positionInList <= updatedInfos.count {
updatedInfos.insert(mappedInfo, at: positionInList)
} else {
updatedInfos.insert(mappedInfo, at: 0)
}
var indexedItems: [ItemCollectionItem] = []
for item in items {
let item = StickerPackItem(index: ItemCollectionItemIndex(index: Int32(indexedItems.count), id: item.index.id), file: item.file._parse(), indexKeys: item.indexKeys)
indexedItems.append(item)
}
transaction.replaceItemCollectionItems(collectionId: mappedInfo.id, items: indexedItems)
}
transaction.replaceItemCollectionInfos(namespace: mappedInfo.id.namespace, itemCollectionInfos: updatedInfos.map { ($0.id, $0) })
}
}
}
public enum RemoveStickerPackOption {
case delete
case archive
}
func _internal_removeStickerPackInteractively(postbox: Postbox, id: ItemCollectionId, option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> {
return _internal_removeStickerPacksInteractively(postbox: postbox, ids: [id], option: option)
}
func _internal_removeStickerPacksInteractively(postbox: Postbox, ids: [ItemCollectionId], option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> {
return postbox.transaction { transaction -> (Int, [ItemCollectionItem])? in
var commonNamespace: SynchronizeInstalledStickerPacksOperationNamespace?
for id in ids {
let namespace: SynchronizeInstalledStickerPacksOperationNamespace?
switch id.namespace {
case Namespaces.ItemCollection.CloudStickerPacks:
namespace = .stickers
case Namespaces.ItemCollection.CloudMaskPacks:
namespace = .masks
case Namespaces.ItemCollection.CloudEmojiPacks:
namespace = .emoji
default:
namespace = nil
}
if commonNamespace == nil && namespace != nil {
commonNamespace = namespace
} else if commonNamespace != namespace {
fatalError()
}
}
if let namespace = commonNamespace {
let content: AddSynchronizeInstalledStickerPacksOperationContent
switch option {
case .delete:
content = .remove(ids)
case .archive:
content = .archive(ids)
}
if let id = ids.first {
let index = transaction.getItemCollectionsInfos(namespace: id.namespace).firstIndex(where: { $0.0 == id })
let items = transaction.getItemCollectionItems(collectionId: id)
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespace, content: content, noDelay: false)
for id in ids {
transaction.removeItemCollection(collectionId: id)
}
return index.flatMap { ($0, items) }
} else {
return nil
}
} else {
return nil
}
}
}
func _internal_markFeaturedStickerPacksAsSeenInteractively(postbox: Postbox, ids: [ItemCollectionId]) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
let idsSet = Set(ids)
var items = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
var readIds = Set<ItemCollectionId>()
for i in 0 ..< items.count {
let item = items[i].contents.get(FeaturedStickerPackItem.self)!
if item.unread && idsSet.contains(item.info.id) {
readIds.insert(item.info.id)
if let entry = CodableEntry(FeaturedStickerPackItem(info: item.info, topItems: item.topItems, unread: false)) {
items[i] = OrderedItemListEntry(id: items[i].id, contents: entry)
}
}
}
if !readIds.isEmpty {
transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks, items: items)
addSynchronizeMarkFeaturedStickerPacksAsSeenOperation(transaction: transaction, ids: Array(readIds))
}
}
}
@@ -0,0 +1,271 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
public enum RequestStickerSetError {
case generic
case invalid
}
public enum RequestStickerSetResult {
case local(info: ItemCollectionInfo, items: [ItemCollectionItem])
case remote(info: ItemCollectionInfo, items: [ItemCollectionItem], installed: Bool)
public var items: [ItemCollectionItem] {
switch self {
case let .local(_, items):
return items
case let .remote(_, items, _):
return items
}
}
}
func _internal_requestStickerSet(postbox: Postbox, network: Network, reference: StickerPackReference) -> Signal<RequestStickerSetResult, RequestStickerSetError> {
let collectionId: ItemCollectionId?
let input: Api.InputStickerSet
switch reference {
case let .name(name):
collectionId = nil
input = .inputStickerSetShortName(shortName: name)
case let .id(id, accessHash):
collectionId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudStickerPacks, id: id)
input = .inputStickerSetID(id: id, accessHash: accessHash)
case .animatedEmoji:
collectionId = nil
input = .inputStickerSetAnimatedEmoji
case let .dice(emoji):
collectionId = nil
input = .inputStickerSetDice(emoticon: emoji)
case .animatedEmojiAnimations:
collectionId = nil
input = .inputStickerSetAnimatedEmojiAnimations
case .premiumGifts:
collectionId = nil
input = .inputStickerSetPremiumGifts
case .emojiGenericAnimations:
collectionId = nil
input = .inputStickerSetEmojiGenericAnimations
case .iconStatusEmoji:
collectionId = nil
input = .inputStickerSetEmojiDefaultStatuses
case .iconChannelStatusEmoji:
collectionId = nil
input = .inputStickerSetEmojiChannelDefaultStatuses
case .iconTopicEmoji:
collectionId = nil
input = .inputStickerSetEmojiDefaultTopicIcons
case .tonGifts:
collectionId = nil
input = .inputStickerSetTonGifts
}
let localSignal: (ItemCollectionId) -> Signal<(ItemCollectionInfo, [ItemCollectionItem])?, NoError> = { collectionId in
return postbox.transaction { transaction -> (ItemCollectionInfo, [ItemCollectionItem])? in
return transaction.getItemCollectionInfoItems(namespace: Namespaces.ItemCollection.CloudStickerPacks, id: collectionId)
}
}
let remoteSignal = network.request(Api.functions.messages.getStickerSet(stickerset: input, hash: 0))
|> mapError { _ -> RequestStickerSetError in
return .invalid
}
|> mapToSignal { result -> Signal<RequestStickerSetResult, RequestStickerSetError> in
var items: [ItemCollectionItem] = []
let info: ItemCollectionInfo
let installed: Bool
switch result {
case .stickerSetNotModified:
return .complete()
case let .stickerSet(set, packs, keywords, documents):
info = StickerPackCollectionInfo(apiSet: set, namespace: Namespaces.ItemCollection.CloudStickerPacks)
switch set {
case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _, _):
installed = (flags & (1 << 0) != 0)
}
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
for pack in packs {
switch pack {
case let .stickerPack(text, fileIds):
let key = ValueBoxKey(text).toMemoryBuffer()
for fileId in fileIds {
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
break
}
}
for keyword in keywords {
switch keyword {
case let .stickerKeyword(documentId, texts):
for text in texts {
let key = ValueBoxKey(text).toMemoryBuffer()
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
for apiDocument in documents {
if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id {
let fileIndexKeys: [MemoryBuffer]
if let indexKeys = indexKeysByFile[id] {
fileIndexKeys = indexKeys
} else {
fileIndexKeys = []
}
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: fileIndexKeys))
}
}
}
return .single(.remote(info: info, items: items, installed: installed))
}
if let collectionId = collectionId {
return localSignal(collectionId) |> mapError { _ -> RequestStickerSetError in } |> mapToSignal { result -> Signal<RequestStickerSetResult, RequestStickerSetError> in
if let result = result {
return .single(.local(info: result.0, items: result.1))
} else {
return remoteSignal
}
}
} else {
return remoteSignal
}
}
public enum InstallStickerSetError {
case generic
}
public enum InstallStickerSetResult {
case successful
case archived([CoveredStickerSet])
}
public final class CoveredStickerSet : Equatable {
let items: [StickerPackItem]
let info: StickerPackCollectionInfo
public init(info: StickerPackCollectionInfo, items: [StickerPackItem]) {
self.items = items
self.info = info
}
public static func ==(lhs: CoveredStickerSet, rhs: CoveredStickerSet) -> Bool {
return lhs.items == rhs.items && lhs.info == rhs.info
}
}
func _internal_installStickerSetInteractively(account: Account, info: StickerPackCollectionInfo, items: [ItemCollectionItem]) -> Signal<InstallStickerSetResult, InstallStickerSetError> {
return account.network.request(Api.functions.messages.installStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), archived: .boolFalse)) |> mapError { _ -> InstallStickerSetError in
return .generic
}
|> mapToSignal { result -> Signal<InstallStickerSetResult, InstallStickerSetError> in
let addResult: InstallStickerSetResult
switch result {
case .stickerSetInstallResultSuccess:
addResult = .successful
case let .stickerSetInstallResultArchive(sets: archived):
var coveredSets: [CoveredStickerSet] = []
for archived in archived {
let apiDocuments:[Api.Document]
let apiSet:Api.StickerSet
switch archived {
case let .stickerSetCovered(set: set, cover: cover):
apiSet = set
apiDocuments = [cover]
case let .stickerSetMultiCovered(set: set, covers: covers):
apiSet = set
apiDocuments = covers
case let .stickerSetFullCovered(set, _, _, documents):
apiSet = set
apiDocuments = documents
case let .stickerSetNoCovered(set):
apiSet = set
apiDocuments = []
}
let info = StickerPackCollectionInfo(apiSet: apiSet, namespace: Namespaces.ItemCollection.CloudStickerPacks)
var items:[StickerPackItem] = []
for apiDocument in apiDocuments {
if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id {
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: []))
}
}
coveredSets.append(CoveredStickerSet(info: info, items: items))
}
addResult = .archived(coveredSets)
}
return account.postbox.transaction { transaction -> Void in
var collections = transaction.getCollectionsItems(namespace: info.id.namespace)
var removableIndexes:[Int] = []
for i in 0 ..< collections.count {
if collections[i].0 == info.id {
removableIndexes.append(i)
}
if case let .archived(sets) = addResult {
for set in sets {
if collections[i].0 == set.info.id {
removableIndexes.append(i)
}
}
}
}
for index in removableIndexes.reversed() {
collections.remove(at: index)
}
collections.insert((info.id, info, items), at: 0)
transaction.replaceItemCollections(namespace: info.id.namespace, itemCollections: collections)
}
|> map { _ in return addResult} |> mapError { _ -> InstallStickerSetError in }
}
}
func _internal_uninstallStickerSetInteractively(account: Account, info: StickerPackCollectionInfo) -> Signal<Void, NoError> {
return account.network.request(Api.functions.messages.uninstallStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { result -> Signal<Void, NoError> in
switch result {
case .boolTrue:
return account.postbox.transaction { transaction -> Void in
var collections = transaction.getCollectionsItems(namespace: info.id.namespace)
for i in 0 ..< collections.count {
if collections[i].0 == info.id {
collections.remove(at: i)
break
}
}
transaction.replaceItemCollections(namespace: info.id.namespace, itemCollections: collections)
}
case .boolFalse:
return .complete()
}
}
}
@@ -0,0 +1,394 @@
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<Void, NoError> {
return _internal_removeArchivedStickerPack(account: self.account, info: info)
}
public func cachedStickerPack(reference: StickerPackReference, forceRemote: Bool) -> Signal<CachedStickerPackResult, NoError> {
return _internal_cachedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceRemote: forceRemote)
}
public func loadedStickerPack(reference: StickerPackReference, forceActualized: Bool, ignoreCache: Bool = false) -> Signal<LoadedStickerPack, NoError> {
return _internal_loadedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceActualized: forceActualized, ignoreCache: ignoreCache)
}
public func randomGreetingSticker() -> Signal<FoundStickerItem?, NoError> {
return _internal_randomGreetingSticker(account: self.account)
}
public func searchStickers(query: String?, emoticon: [String], inputLanguageCode: String = "", scope: SearchStickersScope = [.installed, .remote]) -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> {
return _internal_searchStickers(account: self.account, query: query, emoticon: emoticon, inputLanguageCode: inputLanguageCode, scope: scope)
}
public func searchStickers(category: EmojiSearchCategories.Group, scope: SearchStickersScope = [.installed, .remote]) -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> {
return _internal_searchStickers(account: self.account, category: category, scope: scope)
}
public func searchStickerSetsRemotely(query: String) -> Signal<FoundStickerSets, NoError> {
return _internal_searchStickerSetsRemotely(network: self.account.network, query: query)
}
public func searchEmojiSetsRemotely(query: String) -> Signal<FoundStickerSets, NoError> {
return _internal_searchEmojiSetsRemotely(postbox: self.account.postbox, network: self.account.network, query: query)
}
public func searchStickerSets(query: String) -> Signal<FoundStickerSets, NoError> {
return _internal_searchStickerSets(postbox: self.account.postbox, query: query)
}
public func searchEmojiSets(query: String) -> Signal<FoundStickerSets, NoError> {
return _internal_searchEmojiSets(postbox: self.account.postbox, query: query)
}
public func searchGifs(query: String, nextOffset: String = "") -> Signal<ChatContextResultCollection?, NoError> {
return _internal_searchGifs(account: self.account, query: query, nextOffset: nextOffset)
}
public func addStickerPackInteractively(info: StickerPackCollectionInfo, items: [StickerPackItem], positionInList: Int? = nil) -> Signal<Void, NoError> {
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<Void, NoError> {
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<UploadStickerStatus, UploadStickerError> {
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<CreateStickerSetStatus, CreateStickerSetError> {
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<Never, RenameStickerSetError> {
return _internal_renameStickerSet(account: self.account, packReference: packReference, title: title)
}
public func deleteStickerSet(packReference: StickerPackReference) -> Signal<Never, DeleteStickerSetError> {
return _internal_deleteStickerSet(account: self.account, packReference: packReference)
}
public func addStickerToStickerSet(packReference: StickerPackReference, sticker: ImportSticker) -> Signal<Bool, AddStickerToSetError> {
return _internal_addStickerToStickerSet(account: self.account, packReference: packReference, sticker: sticker)
}
public func reorderSticker(sticker: FileMediaReference, position: Int) -> Signal<Never, ReorderStickerError> {
return _internal_reorderSticker(account: self.account, sticker: sticker, position: position)
}
public func deleteStickerFromStickerSet(sticker: FileMediaReference) -> Signal<Never, DeleteStickerError> {
return _internal_deleteStickerFromStickerSet(account: self.account, sticker: sticker)
}
public func replaceSticker(previousSticker: FileMediaReference, sticker: ImportSticker) -> Signal<Never, ReplaceStickerError> {
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<String?, NoError> {
return _internal_getStickerSetShortNameSuggestion(account: self.account, title: title)
}
public func toggleStickerSaved(file: TelegramMediaFile, saved: Bool) -> Signal<SavedStickerResult, AddSavedStickerError> {
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<AddressNameValidationStatus, NoError> {
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<AvailableReactions?, NoError> {
return _internal_cachedAvailableReactions(postbox: self.account.postbox)
}
public func availableMessageEffects() -> Signal<AvailableMessageEffects?, NoError> {
return _internal_cachedAvailableMessageEffects(postbox: self.account.postbox)
}
public func savedMessageTagData() -> Signal<SavedMessageTags?, NoError> {
return self.account.postbox.combinedView(keys: [PostboxViewKey.cachedItem(_internal_savedMessageTagsCacheKey())])
|> mapToSignal { views -> Signal<SavedMessageTags?, NoError> 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<Set<EnginePeer.Id?>>(value: Set())
public func refreshSavedMessageTags(subPeerId: EnginePeer.Id?) -> Signal<Never, NoError> {
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<Never, NoError> {
return _internal_setSavedMessageTagTitle(account: self.account, reaction: reaction, title: title)
}
public func emojiSearchCategories(kind: EmojiSearchCategories.Kind) -> Signal<EmojiSearchCategories?, NoError> {
return _internal_cachedEmojiSearchCategories(postbox: self.account.postbox, kind: kind)
}
public func updateQuickReaction(reaction: MessageReaction.Reaction) -> Signal<Never, NoError> {
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<Bool, NoError> {
return self.account.postbox.transaction { transaction -> Bool in
return getIsStickerSaved(transaction: transaction, fileId: id)
}
}
public func isGifSaved(id: EngineMedia.Id) -> Signal<Bool, NoError> {
return self.account.postbox.transaction { transaction -> Bool in
return getIsGifSaved(transaction: transaction, mediaId: id)
}
}
public func clearRecentlyUsedStickers() -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
_internal_clearRecentlyUsedStickers(transaction: transaction)
}
|> ignoreValues
}
public func clearRecentlyUsedEmoji() -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
_internal_clearRecentlyUsedEmoji(transaction: transaction)
}
|> ignoreValues
}
public func clearRecentlyUsedReactions() -> Signal<Never, NoError> {
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<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}
public func reorderStickerPacks(namespace: ItemCollectionId.Namespace, itemIds: [ItemCollectionId]) -> Signal<Never, NoError> {
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<ItemCollectionId>()
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(query: String?, emoticon: [String], inputLanguageCode: String = "") -> Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError> {
return _internal_searchEmoji(account: self.account, query: query, emoticon: emoticon, inputLanguageCode: inputLanguageCode)
|> map { items, isFinalResult -> (items: [TelegramMediaFile], isFinalResult: Bool) in
return (items.map(\.file), isFinalResult)
}
}
public func searchEmoji(category: EmojiSearchCategories.Group) -> Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError> {
return _internal_searchEmoji(account: self.account, query: nil, emoticon: category.identifiers, inputLanguageCode: "")
|> 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<Int64>()
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, altDocuments: []) {
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)
}
}