import Foundation import Postbox import SwiftSignalKit import TelegramApi import MtProtoKit private final class ManagedSynchronizeInstalledStickerPacksOperationsHelper { var operationDisposables: [Int32: Disposable] = [:] func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) { var disposeOperations: [Disposable] = [] var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = [] var hasRunningOperationForPeerId = Set() var validMergedIndices = Set() for entry in entries { if !hasRunningOperationForPeerId.contains(entry.peerId) { hasRunningOperationForPeerId.insert(entry.peerId) validMergedIndices.insert(entry.mergedIndex) if self.operationDisposables[entry.mergedIndex] == nil { let disposable = MetaDisposable() beginOperations.append((entry, disposable)) self.operationDisposables[entry.mergedIndex] = disposable } } } var removeMergedIndices: [Int32] = [] for (mergedIndex, disposable) in self.operationDisposables { if !validMergedIndices.contains(mergedIndex) { removeMergedIndices.append(mergedIndex) disposeOperations.append(disposable) } } for mergedIndex in removeMergedIndices { self.operationDisposables.removeValue(forKey: mergedIndex) } return (disposeOperations, beginOperations) } func reset() -> [Disposable] { let disposables = Array(self.operationDisposables.values) self.operationDisposables.removeAll() return disposables } } private func withTakenOperation(postbox: Postbox, peerId: PeerId, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal) -> Signal { return postbox.transaction { transaction -> Signal in var result: PeerMergedOperationLogEntry? transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeInstalledStickerPacksOperation { result = entry.mergedEntry! return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) } else { return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) } }) return f(transaction, result) } |> switchToLatest } func managedSynchronizeInstalledStickerPacksOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager, namespace: SynchronizeInstalledStickerPacksOperationNamespace) -> Signal { return Signal { _ in let tag: PeerOperationLogTag switch namespace { case .stickers: tag = OperationLogTags.SynchronizeInstalledStickerPacks case .masks: tag = OperationLogTags.SynchronizeInstalledMasks case .emoji: tag = OperationLogTags.SynchronizeInstalledEmoji } let helper = Atomic(value: ManagedSynchronizeInstalledStickerPacksOperationsHelper()) let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in return helper.update(view.entries) } for disposable in disposeOperations { disposable.dispose() } for (entry, disposable) in beginOperations { let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal in if let entry = entry { if let operation = entry.contents as? SynchronizeInstalledStickerPacksOperation { return stateManager.isUpdating |> filter { !$0 } |> take(1) |> mapToSignal { _ -> Signal in return postbox.transaction { transaction -> Signal in return synchronizeInstalledStickerPacks(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, namespace: namespace, operation: operation) } |> switchToLatest } } else { assertionFailure() } } return .complete() }) |> then(postbox.transaction { transaction -> Void in let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex) }) disposable.set((signal |> delay(0.0, queue: Queue.concurrentDefaultQueue())).start()) } }) return ActionDisposable { let disposables = helper.with { helper -> [Disposable] in return helper.reset() } for disposable in disposables { disposable.dispose() } disposable.dispose() } } } private func hashForStickerPackInfos(_ infos: [StickerPackCollectionInfo]) -> Int64 { var acc: UInt64 = 0 for info in infos { combineInt64Hash(&acc, with: UInt64(bitPattern: Int64(info.hash))) } return finalizeInt64Hash(acc) } private enum SynchronizeInstalledStickerPacksError { case restart case done case frozen } private func fetchStickerPack(network: Network, info: StickerPackCollectionInfo) -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> { return network.request(Api.functions.messages.getStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), hash: 0)) |> map { result -> (StickerPackCollectionInfo, [ItemCollectionItem]) in var items: [ItemCollectionItem] = [] var updatedInfo = info switch result { case .stickerSetNotModified: break case let .stickerSet(stickerSet, packs, keywords, documents): updatedInfo = StickerPackCollectionInfo(apiSet: stickerSet, namespace: info.id.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) } } 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)) } } break } return (updatedInfo, items) } |> `catch` { _ -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> in return .single((info, [])) } } private func resolveStickerPacks(network: Network, remoteInfos: [ItemCollectionId: (StickerPackCollectionInfo, Bool)], localInfos: [ItemCollectionId: StickerPackCollectionInfo]) -> Signal<[ItemCollectionId: [ItemCollectionItem]], NoError> { var signals: [Signal<(ItemCollectionId, [ItemCollectionItem]), NoError>] = [] for (id, remoteInfoAndIsStale) in remoteInfos { let (remoteInfo, isStale) = remoteInfoAndIsStale let localInfo = localInfos[id] if localInfo == nil || localInfo!.hash != remoteInfo.hash || isStale { signals.append(fetchStickerPack(network: network, info: remoteInfo) |> map { (info, items) in return (info.id, items) }) } } return combineLatest(signals) |> map { result -> [ItemCollectionId: [ItemCollectionItem]] in var dict: [ItemCollectionId: [ItemCollectionItem]] = [:] for (id, items) in result { dict[id] = items } return dict } } private func installRemoteStickerPacks(network: Network, infos: [StickerPackCollectionInfo]) -> Signal, NoError> { var signals: [Signal, NoError>] = [] for info in infos { let install = network.request(Api.functions.messages.installStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), archived: .boolFalse)) |> map { result -> Set in switch result { case .stickerSetInstallResultSuccess: return Set() case let .stickerSetInstallResultArchive(archivedSets): var archivedIds = Set() for archivedSet in archivedSets { switch archivedSet { case let .stickerSetCovered(set, _): archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) case let .stickerSetMultiCovered(set, _): archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) case let .stickerSetFullCovered(set, _, _, _): archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) case let .stickerSetNoCovered(set): archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) } } return archivedIds } } |> `catch` { _ -> Signal, NoError> in return .single(Set()) } signals.append(install) } return combineLatest(signals) |> map { idsSets -> Set in var result = Set() for ids in idsSets { result.formUnion(ids) } return result } } private func removeRemoteStickerPacks(network: Network, infos: [StickerPackCollectionInfo]) -> Signal { if infos.count > 0 { if infos.count > 1 { return network.request(Api.functions.messages.toggleStickerSets(flags: 1 << 0, stickersets: infos.map { .inputStickerSetID(id: $0.id.id, accessHash: $0.accessHash) })) |> mapToSignal { _ -> Signal in return .single(Void()) } |> `catch` { _ -> Signal in return .single(Void()) } } else if let info = infos.first { return network.request(Api.functions.messages.uninstallStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash))) |> mapToSignal { _ -> Signal in return .single(Void()) } |> `catch` { _ -> Signal in return .single(Void()) } } } return .single(Void()) } private func archiveRemoteStickerPacks(network: Network, infos: [StickerPackCollectionInfo]) -> Signal { if infos.count > 0 { if infos.count > 1 { return network.request(Api.functions.messages.toggleStickerSets(flags: 1 << 1, stickersets: infos.map { .inputStickerSetID(id: $0.id.id, accessHash: $0.accessHash) })) |> mapToSignal { _ -> Signal in return .single(Void()) } |> `catch` { _ -> Signal in return .single(Void()) } } else if let info = infos.first { return network.request(Api.functions.messages.installStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), archived: .boolTrue)) |> mapToSignal { _ -> Signal in return .single(Void()) } |> `catch` { _ -> Signal in return .single(Void()) } } } return .single(Void()) } private func reorderRemoteStickerPacks(network: Network, namespace: SynchronizeInstalledStickerPacksOperationNamespace, ids: [ItemCollectionId]) -> Signal { var flags: Int32 = 0 switch namespace { case .stickers: break case .masks: flags |= (1 << 0) case .emoji: flags |= (1 << 1) } return network.request(Api.functions.messages.reorderStickerSets(flags: flags, order: ids.map { $0.id })) |> `catch` { _ -> Signal in return .single(.boolFalse) } |> mapToSignal { _ -> Signal in return .complete() } } private func synchronizeInstalledStickerPacks(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, namespace: SynchronizeInstalledStickerPacksOperationNamespace, operation: SynchronizeInstalledStickerPacksOperation) -> Signal { let collectionNamespace: ItemCollectionId.Namespace switch namespace { case .stickers: collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks case .masks: collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks case .emoji: collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks } let localCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo } let localStateCollectionOrder = localCollectionInfos.map({ $0.id }) let localPreviousStateCollectionOrder = operation.previousPacks if localStateCollectionOrder == localPreviousStateCollectionOrder { return continueSynchronizeInstalledStickerPacks( transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, namespace: namespace, operation: operation ) } let locallyAddedInfos = localCollectionInfos.filter { !localPreviousStateCollectionOrder.contains($0.id) } if locallyAddedInfos.isEmpty { return continueSynchronizeInstalledStickerPacks( transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, namespace: namespace, operation: operation ) } var fetchSignals: [Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError>] = [] for info in locallyAddedInfos { fetchSignals.append(fetchStickerPack(network: network, info: info)) } let fetchedPacks = combineLatest(fetchSignals) return fetchedPacks |> mapToSignal { fetchedPacks -> Signal in return postbox.transaction { transaction -> Signal in var currentInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo } for (info, items) in fetchedPacks { if let index = currentInfos.firstIndex(where: { $0.id == info.id }) { currentInfos[index] = info transaction.replaceItemCollectionItems(collectionId: info.id, items: items) } } transaction.replaceItemCollectionInfos(namespace: collectionNamespace, itemCollectionInfos: currentInfos.map { ($0.id, $0) }) return continueSynchronizeInstalledStickerPacks( transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, namespace: namespace, operation: operation ) } |> switchToLatest } } /*#if DEBUG func debugFetchAllStickers(account: Account) -> Signal { let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers] let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerItems: Signal<[TelegramMediaFile], NoError> = account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000) |> map { view -> [TelegramMediaFile] in var files: [TelegramMediaFile] = [] for entry in view.entries { guard let item = entry.item as? StickerPackItem else { continue } if !item.file.isAnimatedSticker { continue } files.append(item.file) } return files } |> take(1) return stickerItems |> mapToSignal { files -> Signal in var loadFileSignals: [Signal] = [] let tempDir = TempBox.shared.tempDirectory() print("debugFetchAllStickers into \(tempDir.path)") for file in files { loadFileSignals.append(Signal { subscriber in let fetch = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: stickerPackFileReference(file).resourceReference(file.resource)).start() let data = (account.postbox.mediaBox.resourceData(file.resource) |> filter { $0.complete } |> take(1)).start(next: { data in if let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let unpackedData = TGGUnzipData(dataValue, 5 * 1024 * 1024) { let filePath = tempDir.path + "/\(file.fileId.id).json" let _ = try? unpackedData.write(to: URL(fileURLWithPath: filePath), options: .atomic) subscriber.putCompletion() } }) return ActionDisposable { fetch.dispose() data.dispose() } }) } return combineLatest(loadFileSignals) |> ignoreValues } } #endif*/ private func continueSynchronizeInstalledStickerPacks(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, namespace: SynchronizeInstalledStickerPacksOperationNamespace, operation: SynchronizeInstalledStickerPacksOperation) -> Signal { let collectionNamespace: ItemCollectionId.Namespace switch namespace { case .stickers: collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks case .masks: collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks case .emoji: collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks } let localCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo } let initialLocalHash = hashForStickerPackInfos(localCollectionInfos) let request: Signal switch namespace { case .stickers: request = network.request(Api.functions.messages.getAllStickers(hash: initialLocalHash)) case .masks: request = network.request(Api.functions.messages.getMaskStickers(hash: initialLocalHash)) case .emoji: request = network.request(Api.functions.messages.getEmojiStickers(hash: initialLocalHash)) } let sequence = request |> retryRequestIfNotFrozen |> mapError { _ -> SynchronizeInstalledStickerPacksError in } |> mapToSignal { result -> Signal in guard let result else { return .fail(.frozen) } return postbox.transaction { transaction -> Signal in let checkLocalCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo } if checkLocalCollectionInfos != localCollectionInfos { return .fail(.restart) } let localInitialStateCollectionOrder = operation.previousPacks let localInitialStateCollectionIds = Set(localInitialStateCollectionOrder) let localCollectionOrder = checkLocalCollectionInfos.map { $0.id } let localCollectionIds = Set(localCollectionOrder) var remoteCollectionInfos: [StickerPackCollectionInfo] = [] switch result { case let .allStickers(_, sets): for apiSet in sets { let info = StickerPackCollectionInfo(apiSet: apiSet, namespace: collectionNamespace) remoteCollectionInfos.append(info) } case .allStickersNotModified: remoteCollectionInfos = checkLocalCollectionInfos } let remoteCollectionOrder = remoteCollectionInfos.map { $0.id } let remoteCollectionIds = Set(remoteCollectionOrder) var remoteInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:] for info in remoteCollectionInfos { remoteInfos[info.id] = info } var localInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:] for info in checkLocalCollectionInfos { localInfos[info.id] = info } if localInitialStateCollectionOrder == localCollectionOrder { if checkLocalCollectionInfos == remoteCollectionInfos { return .fail(.done) } else { var resolveRemoteInfos: [ItemCollectionId: (StickerPackCollectionInfo, Bool)] = [:] for (id, info) in remoteInfos { resolveRemoteInfos[id] = (info, false) } return resolveStickerPacks(network: network, remoteInfos: resolveRemoteInfos, localInfos: localInfos) |> mapError { _ -> SynchronizeInstalledStickerPacksError in } |> mapToSignal { replaceItems -> Signal in return postbox.transaction { transaction -> Signal in let storeSignal: Signal if localCollectionIds.isEmpty { var incrementalSignal = postbox.transaction { transaction -> Void in transaction.replaceItemCollectionInfos(namespace: collectionNamespace, itemCollectionInfos: remoteCollectionInfos.map { ($0.id, $0) }) for id in localCollectionIds.subtracting(remoteCollectionIds) { transaction.replaceItemCollectionItems(collectionId: id, items: []) } } for (id, items) in replaceItems { let partSignal = postbox.transaction { transaction -> Void in transaction.replaceItemCollectionItems(collectionId: id, items: items) } incrementalSignal = incrementalSignal |> then( partSignal |> delay(0.01, queue: Queue.concurrentDefaultQueue()) ) } storeSignal = incrementalSignal } else { storeSignal = postbox.transaction { transaction -> Void in transaction.replaceItemCollectionInfos(namespace: collectionNamespace, itemCollectionInfos: remoteCollectionInfos.map { ($0.id, $0) }) for (id, items) in replaceItems { transaction.replaceItemCollectionItems(collectionId: id, items: items) } for id in localCollectionIds.subtracting(remoteCollectionIds) { transaction.replaceItemCollectionItems(collectionId: id, items: []) } } } let storePremiumSignal: Signal = postbox.transaction { transaction -> Void in var premiumStickers: [OrderedItemListEntry] = [] for (id, _) in remoteInfos { let items = transaction.getItemCollectionItems(collectionId: id) for item in items { if let stickerItem = item as? StickerPackItem, stickerItem.file.isPremiumSticker, let entry = CodableEntry(RecentMediaItem(stickerItem.file._parse())) { premiumStickers.append(OrderedItemListEntry(id: RecentMediaItemId(stickerItem.file.fileId).rawValue, contents: entry)) } } } transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.PremiumStickers, items: premiumStickers) } |> castError(SynchronizeInstalledStickerPacksError.self) return ( storeSignal |> mapError { _ -> SynchronizeInstalledStickerPacksError in } ) |> then(storePremiumSignal) |> then(.fail(.done)) } |> castError(SynchronizeInstalledStickerPacksError.self) |> switchToLatest } } } else { let locallyRemovedCollectionIds = localInitialStateCollectionIds.subtracting(localCollectionIds) let locallyAddedCollectionIds = localCollectionIds.subtracting(localInitialStateCollectionIds) let remotelyAddedCollections = remoteCollectionInfos.filter { info in return !locallyRemovedCollectionIds.contains(info.id) && !localCollectionIds.contains(info.id) } let remotelyRemovedCollectionIds = remoteCollectionIds.subtracting(localInitialStateCollectionIds).subtracting(locallyAddedCollectionIds) var resultingCollectionInfos: [(StickerPackCollectionInfo, Bool)] = [] resultingCollectionInfos.append(contentsOf: remotelyAddedCollections.map { ($0, false) }) var remoteCollectionInfoMap: [ItemCollectionId: StickerPackCollectionInfo] = [:] for info in remoteCollectionInfos { remoteCollectionInfoMap[info.id] = info } for info in checkLocalCollectionInfos { if !remotelyRemovedCollectionIds.contains(info.id) { if let remoteInfo = remoteCollectionInfoMap[info.id] { resultingCollectionInfos.append((remoteInfo, false)) } else { resultingCollectionInfos.append((info, true)) } } } let resultingCollectionIds = Set(resultingCollectionInfos.map { $0.0.id }) let removeRemoteCollectionIds = remoteCollectionIds.subtracting(resultingCollectionIds) let addRemoteCollectionIds = resultingCollectionIds.subtracting(remoteCollectionIds) var removeRemoteCollectionInfos: [StickerPackCollectionInfo] = [] for id in removeRemoteCollectionIds { if let info = remoteInfos[id] { removeRemoteCollectionInfos.append(info) } else if let info = localInfos[id] { removeRemoteCollectionInfos.append(info) } } var addRemoteCollectionInfos: [StickerPackCollectionInfo] = [] for id in addRemoteCollectionIds { if let info = remoteInfos[id] { addRemoteCollectionInfos.append(info) } else if let info = localInfos[id] { addRemoteCollectionInfos.append(info) } } let infosToArchive = removeRemoteCollectionInfos.filter { info in return operation.archivedPacks.contains(info.id) } let infosToRemove = removeRemoteCollectionInfos.filter { info in return !operation.archivedPacks.contains(info.id) } let archivedOrRemovedIds = (combineLatest(archiveRemoteStickerPacks(network: network, infos: infosToArchive), removeRemoteStickerPacks(network: network, infos: infosToRemove)) |> mapToSignal { _ -> Signal in return .complete() } |> then(Signal.single(Void()))) |> mapToSignal { _ -> Signal, NoError> in return installRemoteStickerPacks(network: network, infos: addRemoteCollectionInfos) |> mapToSignal { ids -> Signal, NoError> in return (reorderRemoteStickerPacks(network: network, namespace: namespace, ids: resultingCollectionInfos.map({ $0.0.id }).filter({ !ids.contains($0) })) |> then(Signal.single(Void()))) |> map { _ -> Set in return ids } } } var resultingInfos: [ItemCollectionId: (StickerPackCollectionInfo, Bool)] = [:] for info in resultingCollectionInfos { resultingInfos[info.0.id] = info } let resolvedItems = resolveStickerPacks(network: network, remoteInfos: resultingInfos, localInfos: localInfos) return combineLatest(archivedOrRemovedIds, resolvedItems) |> mapError { _ -> SynchronizeInstalledStickerPacksError in } |> mapToSignal { archivedOrRemovedIds, replaceItems -> Signal in return (postbox.transaction { transaction -> Signal in let finalCheckLocalCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo } if finalCheckLocalCollectionInfos != localCollectionInfos { return .fail(.restart) } transaction.replaceItemCollectionInfos(namespace: collectionNamespace, itemCollectionInfos: resultingCollectionInfos.filter({ info in return !archivedOrRemovedIds.contains(info.0.id) }).map({ ($0.0.id, $0.0) })) for (id, items) in replaceItems { if !archivedOrRemovedIds.contains(id) { transaction.replaceItemCollectionItems(collectionId: id, items: items) } } for id in localCollectionIds.subtracting(resultingCollectionIds).union(archivedOrRemovedIds) { transaction.replaceItemCollectionItems(collectionId: id, items: []) } var premiumStickers: [OrderedItemListEntry] = [] for id in resultingCollectionIds { let items = transaction.getItemCollectionItems(collectionId: id) for item in items { if let stickerItem = item as? StickerPackItem, stickerItem.file.isPremiumSticker, let entry = CodableEntry(RecentMediaItem(stickerItem.file._parse())) { premiumStickers.append(OrderedItemListEntry(id: RecentMediaItemId(stickerItem.file.fileId).rawValue, contents: entry)) } } } transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.PremiumStickers, items: premiumStickers) return .complete() } |> mapError { _ -> SynchronizeInstalledStickerPacksError in }) |> switchToLatest |> then(.fail(.done)) } } } |> mapError { _ -> SynchronizeInstalledStickerPacksError in } |> switchToLatest } return ((sequence |> `catch` { error -> Signal in switch error { case .frozen: return .fail(.frozen) case .done: return .fail(.done) case .restart: return .complete() } }) |> restart) |> `catch` { _ -> Signal in return .complete() } }