diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index d3450a6f61..722bff1e4e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -144,6 +144,7 @@ public struct Namespaces { public static let channelsForPublicReaction: Int8 = 45 public static let cachedGroupsInCommon: Int8 = 46 public static let groupCallPersistentSettings: Int8 = 47 + public static let cachedProfileGiftsCollections: Int8 = 48 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 280bf23c6c..da54971bd6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -1162,9 +1162,16 @@ private final class CachedProfileGifts: Codable { } } -private func entryId(peerId: EnginePeer.Id) -> ItemCacheEntryId { - let cacheKey = ValueBoxKey(length: 8) - cacheKey.setInt64(0, value: peerId.toInt64()) +private func entryId(peerId: EnginePeer.Id, collectionId: Int32?) -> ItemCacheEntryId { + let cacheKey: ValueBoxKey + if let collectionId { + cacheKey = ValueBoxKey(length: 8 + 4) + cacheKey.setInt64(0, value: peerId.toInt64()) + cacheKey.setInt32(8, value: collectionId) + } else { + cacheKey = ValueBoxKey(length: 8) + cacheKey.setInt64(0, value: peerId.toInt64()) + } return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedProfileGifts, key: cacheKey) } @@ -1172,6 +1179,7 @@ private final class ProfileGiftsContextImpl { private let queue: Queue private let account: Account private let peerId: PeerId + private let collectionId: Int32? private let disposable = MetaDisposable() private let cacheDisposable = MetaDisposable() @@ -1200,12 +1208,14 @@ private final class ProfileGiftsContextImpl { queue: Queue, account: Account, peerId: EnginePeer.Id, + collectionId: Int32?, sorting: ProfileGiftsContext.Sorting, filter: ProfileGiftsContext.Filters ) { self.queue = queue self.account = account self.peerId = peerId + self.collectionId = collectionId self.sorting = sorting self.filter = filter @@ -1226,6 +1236,7 @@ private final class ProfileGiftsContextImpl { func loadMore(reload: Bool = false) { let peerId = self.peerId + let collectionId = self.collectionId let accountPeerId = self.account.peerId let network = self.account.network let postbox = self.account.postbox @@ -1244,7 +1255,7 @@ private final class ProfileGiftsContextImpl { if case let .ready(true, initialNextOffset) = dataState { if !isFiltered || isUniqueOnlyFilter, self.gifts.isEmpty, initialNextOffset == nil, !reload { self.cacheDisposable.set((self.account.postbox.transaction { transaction -> CachedProfileGifts? in - let cachedGifts = transaction.retrieveItemCacheEntry(id: entryId(peerId: peerId))?.get(CachedProfileGifts.self) + let cachedGifts = transaction.retrieveItemCacheEntry(id: entryId(peerId: peerId, collectionId: collectionId))?.get(CachedProfileGifts.self) cachedGifts?.render(transaction: transaction) return cachedGifts } |> deliverOn(self.queue)).start(next: { [weak self] cachedGifts in @@ -1292,6 +1303,9 @@ private final class ProfileGiftsContextImpl { return .single(([], 0, nil, nil)) } var flags: Int32 = 0 + if let _ = collectionId { + flags |= (1 << 6) + } if case .value = sorting { flags |= (1 << 5) } @@ -1310,7 +1324,7 @@ private final class ProfileGiftsContextImpl { if !filter.contains(.unique) { flags |= (1 << 4) } - return network.request(Api.functions.payments.getSavedStarGifts(flags: flags, peer: inputPeer, collectionId: nil, offset: initialNextOffset ?? "", limit: 36)) + return network.request(Api.functions.payments.getSavedStarGifts(flags: flags, peer: inputPeer, collectionId: collectionId, offset: initialNextOffset ?? "", limit: 36)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -1363,7 +1377,7 @@ private final class ProfileGiftsContextImpl { self.gifts = gifts self.cacheDisposable.set(self.account.postbox.transaction { transaction in if let entry = CodableEntry(CachedProfileGifts(gifts: gifts, count: count, notificationsEnabled: notificationsEnabled)) { - transaction.putItemCacheEntry(id: entryId(peerId: peerId), entry: entry) + transaction.putItemCacheEntry(id: entryId(peerId: peerId, collectionId: collectionId), entry: entry) } }.start()) } else { @@ -2063,12 +2077,13 @@ public final class ProfileGiftsContext { public init( account: Account, peerId: EnginePeer.Id, + collectionId: Int32? = nil, sorting: ProfileGiftsContext.Sorting = .date, filter: ProfileGiftsContext.Filters = .All ) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return ProfileGiftsContextImpl(queue: queue, account: account, peerId: peerId, sorting: sorting, filter: filter) + return ProfileGiftsContextImpl(queue: queue, account: account, peerId: peerId, collectionId: collectionId, sorting: sorting, filter: filter) }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsCollections.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsCollections.swift new file mode 100644 index 0000000000..441244bde9 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsCollections.swift @@ -0,0 +1,407 @@ +import Foundation +import Postbox +import MtProtoKit +import SwiftSignalKit +import TelegramApi + +public struct StarGiftCollection: Codable, Equatable { + public let id: Int32 + public let title: String + public let icon: TelegramMediaFile? + public let count: Int32 + public let hash: Int64 + + public init(id: Int32, title: String, icon: TelegramMediaFile?, count: Int32, hash: Int64) { + self.id = id + self.title = title + self.icon = icon + self.count = count + self.hash = hash + } + + public static func ==(lhs: StarGiftCollection, rhs: StarGiftCollection) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.icon != rhs.icon { + return false + } + if lhs.count != rhs.count { + return false + } + if lhs.hash != rhs.hash { + return false + } + return true + } +} + +extension StarGiftCollection { + init?(apiStarGiftCollection: Api.StarGiftCollection) { + switch apiStarGiftCollection { + case let .starGiftCollection(_, collectionId, title, icon, giftsCount, hash): + self.id = collectionId + self.title = title + self.icon = icon.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: nil) } + self.count = giftsCount + self.hash = hash + } + } +} + +private final class CachedProfileGiftsCollections: Codable { + enum CodingKeys: String, CodingKey { + case collections + } + + let collections: [StarGiftCollection] + + init(collections: [StarGiftCollection]) { + self.collections = collections + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.collections = try container.decode([StarGiftCollection].self, forKey: .collections) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.collections, forKey: .collections) + } +} + +private func entryId(peerId: EnginePeer.Id) -> ItemCacheEntryId { + let cacheKey = ValueBoxKey(length: 8) + cacheKey.setInt64(0, value: peerId.toInt64()) + return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedProfileGiftsCollections, key: cacheKey) +} + +private func intListSimpleHash(_ list: [Int64]) -> Int64 { + var acc: Int64 = 0 + for value in list { + acc = ((acc * 20261) + Int64(0x80000000) + Int64(value)) % Int64(0x80000000) + } + return Int64(Int32(truncatingIfNeeded: acc)) +} + +private func _internal_getStarGiftCollections(postbox: Postbox, network: Network, peerId: EnginePeer.Id) -> Signal<[StarGiftCollection]?, NoError> { + return postbox.transaction { transaction -> (Api.InputPeer, [StarGiftCollection]?)? in + guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) else { + return nil + } + let collections = transaction.retrieveItemCacheEntry(id: entryId(peerId: peerId))?.get(CachedProfileGiftsCollections.self) + return (inputPeer, collections?.collections) + } + |> mapToSignal { inputPeerAndHash -> Signal<[StarGiftCollection]?, NoError> in + guard let (inputPeer, cachedCollections) = inputPeerAndHash else { + return .single(nil) + } + + var hash: Int64 = 0 + if let cachedCollections { + hash = intListSimpleHash(cachedCollections.map { $0.hash }) + } + + return network.request(Api.functions.payments.getStarGiftCollections(peer: inputPeer, hash: hash)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<[StarGiftCollection]?, NoError> in + guard let result else { + return .single(nil) + } + return postbox.transaction { transaction -> [StarGiftCollection]? in + switch result { + case let .starGiftCollections(collections): + let collections = collections.compactMap { StarGiftCollection(apiStarGiftCollection: $0) } + if let entry = CodableEntry(CachedProfileGiftsCollections(collections: collections)) { + transaction.putItemCacheEntry(id: entryId(peerId: peerId), entry: entry) + } + return collections + case .starGiftCollectionsNotModified: + return cachedCollections ?? [] + } + } + } + } +} + +private func _internal_createStarGiftCollection(account: Account, peerId: EnginePeer.Id, title: String, starGifts: [StarGiftReference]) -> Signal { + return account.postbox.transaction { transaction -> (Api.InputPeer, [Api.InputSavedStarGift])? in + guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) else { + return nil + } + let inputStarGifts = starGifts.compactMap { $0.apiStarGiftReference(transaction: transaction) } + return (inputPeer, inputStarGifts) + } + |> mapToSignal { inputPeerAndGifts -> Signal in + guard let (inputPeer, inputStarGifts) = inputPeerAndGifts else { + return .single(nil) + } + + return account.network.request(Api.functions.payments.createStarGiftCollection(peer: inputPeer, title: title, stargift: inputStarGifts)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> StarGiftCollection? in + guard let result else { + return nil + } + return StarGiftCollection(apiStarGiftCollection: result) + } + } +} + +private func _internal_reorderStarGiftCollections(account: Account, peerId: EnginePeer.Id, order: [Int32]) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .single(false) + } + + return account.network.request(Api.functions.payments.reorderStarGiftCollections(peer: inputPeer, order: order)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> Bool in + if let result, case .boolTrue = result { + return true + } + return false + } + } +} + +private func _internal_updateStarGiftCollection(account: Account, peerId: EnginePeer.Id, collectionId: Int32, actions: [StarGiftCollectionsContext.UpdateAction]) -> Signal { + return account.postbox.transaction { transaction -> (Api.InputPeer, (FunctionDescription, Buffer, DeserializeFunctionResponse))? in + guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) else { + return nil + } + + var flags: Int32 = 0 + var title: String? + var deleteStarGift: [Api.InputSavedStarGift] = [] + var addStarGift: [Api.InputSavedStarGift] = [] + var order: [Api.InputSavedStarGift] = [] + + for action in actions { + switch action { + case let .updateTitle(newTitle): + flags |= (1 << 0) + title = newTitle + case let .addGifts(gifts): + flags |= (1 << 2) + addStarGift.append(contentsOf: gifts.compactMap { $0.apiStarGiftReference(transaction: transaction) }) + case let .removeGifts(gifts): + flags |= (1 << 1) + deleteStarGift.append(contentsOf: gifts.compactMap { $0.apiStarGiftReference(transaction: transaction) }) + case let .reorderGifts(gifts): + flags |= (1 << 3) + order = gifts.compactMap { $0.apiStarGiftReference(transaction: transaction) } + } + } + + let request = Api.functions.payments.updateStarGiftCollection(flags: flags, peer: inputPeer, collectionId: collectionId, title: title, deleteStargift: deleteStarGift, addStargift: addStarGift, order: order) + + return (inputPeer, request) + } + |> mapToSignal { peerAndRequest -> Signal in + guard let (_, request) = peerAndRequest else { + return .single(nil) + } + + return account.network.request(request) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> StarGiftCollection? in + guard let result else { + return nil + } + return StarGiftCollection(apiStarGiftCollection: result) + } + } +} + +private func _internal_deleteStarGiftCollection(account: Account, peerId: EnginePeer.Id, collectionId: Int32) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .single(false) + } + + return account.network.request(Api.functions.payments.deleteStarGiftCollection(peer: inputPeer, collectionId: collectionId)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> Bool in + if let result, case .boolTrue = result { + return true + } + return false + } + } +} + +public final class StarGiftCollectionsContext { + public struct State: Equatable { + public var collections: [StarGiftCollection] + public var isLoading: Bool + } + + public enum UpdateAction { + case updateTitle(String) + case addGifts([StarGiftReference]) + case removeGifts([StarGiftReference]) + case reorderGifts([StarGiftReference]) + } + + private let queue: Queue = .mainQueue() + private let account: Account + private let peerId: EnginePeer.Id + + private let disposable = MetaDisposable() + + private var collections: [StarGiftCollection] = [] + private var isLoading: Bool = false + + private let stateValue = Promise() + public var state: Signal { + return self.stateValue.get() + } + + public init(account: Account, peerId: EnginePeer.Id) { + self.account = account + self.peerId = peerId + + self.reload() + } + + deinit { + self.disposable.dispose() + } + + public func reload() { + guard !self.isLoading else { return } + + self.isLoading = true + self.pushState() + + self.disposable.set((_internal_getStarGiftCollections(postbox: self.account.postbox, network: self.account.network, peerId: self.peerId) + |> deliverOn(self.queue)).start(next: { [weak self] collections in + guard let self else { + return + } + self.collections = collections ?? [] + self.isLoading = false + self.pushState() + })) + } + + public func createCollection(title: String, starGifts: [StarGiftReference]) -> Signal { + return _internal_createStarGiftCollection(account: self.account, peerId: self.peerId, title: title, starGifts: starGifts) + |> deliverOn(self.queue) + |> afterNext { [weak self] collection in + guard let self else { + return + } + if let collection { + self.collections.append(collection) + self.pushState() + } + self.reload() + } + } + + private func updateCollection(id: Int32, actions: [UpdateAction]) -> Signal { + return _internal_updateStarGiftCollection(account: self.account, peerId: self.peerId, collectionId: id, actions: actions) + |> deliverOn(self.queue) + |> afterNext { [weak self] collection in + guard let self else { + return + } + if let collection { + if let index = self.collections.firstIndex(where: { $0.id == id }) { + self.collections[index] = collection + self.pushState() + } + } + self.reload() + } + } + + public func addGifts(id: Int32, gifts: [StarGiftReference]) -> Signal { + return self.updateCollection(id: id, actions: [.addGifts(gifts)]) + } + + public func removeGifts(id: Int32, gifts: [StarGiftReference]) -> Signal { + return self.updateCollection(id: id, actions: [.addGifts(gifts)]) + } + + public func reorderGifts(id: Int32, gifts: [StarGiftReference]) -> Signal { + return self.updateCollection(id: id, actions: [.reorderGifts(gifts)]) + } + + public func renameCollection(id: Int32, title: String) -> Signal { + return self.updateCollection(id: id, actions: [.updateTitle(title)]) + } + + public func reorderCollections(order: [Int32]) -> Signal { + return _internal_reorderStarGiftCollections(account: self.account, peerId: self.peerId, order: order) + |> deliverOn(self.queue) + |> afterNext { [weak self] collection in + guard let self else { + return + } + var collectionMap: [Int32: StarGiftCollection] = [:] + for collection in self.collections { + collectionMap[collection.id] = collection + } + var collections: [StarGiftCollection] = [] + for id in order { + if let collection = collectionMap[id] { + collections.append(collection) + } + } + self.collections = collections + self.pushState() + self.reload() + } + } + + public func deleteCollection(id: Int32) -> Signal { + return _internal_deleteStarGiftCollection(account: self.account, peerId: self.peerId, collectionId: id) + |> deliverOn(self.queue) + |> afterNext { [weak self] _ in + guard let self else { + return + } + self.collections.removeAll(where: { $0.id == id }) + self.pushState() + self.reload() + } + } + + private func pushState() { + let state = State( + collections: self.collections, + isLoading: self.isLoading + ) + self.stateValue.set(.single(state)) + } +}