diff --git a/submodules/TelegramCore/Sources/State/ChannelBoost.swift b/submodules/TelegramCore/Sources/State/ChannelBoost.swift index fd74a4dde3..b09f8f4f2d 100644 --- a/submodules/TelegramCore/Sources/State/ChannelBoost.swift +++ b/submodules/TelegramCore/Sources/State/ChannelBoost.swift @@ -5,15 +5,15 @@ import SwiftSignalKit public final class ChannelBoostStatus: Equatable { public let level: Int - public let currentLevelBoosts: Int public let boosts: Int + public let currentLevelBoosts: Int public let nextLevelBoosts: Int? public let premiumAudience: StatsPercentValue? - public init(level: Int, currentLevelBoosts: Int, boosts: Int, nextLevelBoosts: Int?, premiumAudience: StatsPercentValue?) { + public init(level: Int, boosts: Int, currentLevelBoosts: Int, nextLevelBoosts: Int?, premiumAudience: StatsPercentValue?) { self.level = level - self.currentLevelBoosts = currentLevelBoosts self.boosts = boosts + self.currentLevelBoosts = currentLevelBoosts self.nextLevelBoosts = nextLevelBoosts self.premiumAudience = premiumAudience } @@ -22,16 +22,13 @@ public final class ChannelBoostStatus: Equatable { if lhs.level != rhs.level { return false } - if lhs.currentLevelBoosts != rhs.currentLevelBoosts { - return false - } if lhs.boosts != rhs.boosts { return false } - if lhs.nextLevelBoosts != rhs.nextLevelBoosts { + if lhs.currentLevelBoosts != rhs.currentLevelBoosts { return false } - if lhs.premiumAudience != rhs.premiumAudience { + if lhs.nextLevelBoosts != rhs.nextLevelBoosts { return false } if lhs.premiumAudience != rhs.premiumAudience { @@ -61,9 +58,7 @@ func _internal_getChannelBoostStatus(account: Account, peerId: PeerId) -> Signal switch result { case let .boostsStatus(_, level, currentLevelBoosts, boosts, nextLevelBoosts, premiumAudience): - return ChannelBoostStatus(level: Int(level), currentLevelBoosts: Int(currentLevelBoosts), boosts: Int(boosts), nextLevelBoosts: nextLevelBoosts.flatMap(Int.init), premiumAudience: premiumAudience.flatMap { value in - return StatsPercentValue(apiPercentValue: value) - }) + return ChannelBoostStatus(level: Int(level), boosts: Int(boosts), currentLevelBoosts: Int(currentLevelBoosts), nextLevelBoosts: nextLevelBoosts.flatMap(Int.init), premiumAudience: premiumAudience.flatMap({ StatsPercentValue(apiPercentValue: $0) })) } } } @@ -73,8 +68,9 @@ public enum CanApplyBoostStatus { public enum ErrorReason { case generic case premiumRequired - case floodWait + case floodWait(Int32) case peerBoostAlreadyActive + case giftedPremiumNotAllowed } case ok @@ -99,9 +95,21 @@ func _internal_canApplyChannelBoost(account: Account, peerId: PeerId) -> Signal< if error.errorDescription == "PREMIUM_ACCOUNT_REQUIRED" { reason = .premiumRequired } else if error.errorDescription.hasPrefix("FLOOD_WAIT_") { - reason = .floodWait - } else if error.errorDescription == "SAME_BOOST_ALREADY_ACTIVE" { + let errorText = error.errorDescription ?? "" + if let underscoreIndex = errorText.lastIndex(of: "_") { + let timeoutText = errorText[errorText.index(after: underscoreIndex)...] + if let timeoutValue = Int32(String(timeoutText)) { + reason = .floodWait(timeoutValue) + } else { + reason = .generic + } + } else { + reason = .generic + } + } else if error.errorDescription == "SAME_BOOST_ALREADY_ACTIVE" || error.errorDescription == "BOOST_NOT_MODIFIED" { reason = .peerBoostAlreadyActive + } else if error.errorDescription == "PREMIUM_GIFTED_NOT_ALLOWED" { + reason = .giftedPremiumNotAllowed } else { reason = .generic } @@ -130,3 +138,346 @@ func _internal_canApplyChannelBoost(account: Account, peerId: PeerId) -> Signal< } } } + +func _internal_applyChannelBoost(account: Account, peerId: PeerId) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .single(false) + } + return account.network.request(Api.functions.stories.applyBoost(peer: inputPeer)) + |> `catch` { error -> Signal in + return .single(.boolFalse) + } + |> map { result -> Bool in + if case .boolTrue = result { + return true + } + return false + } + } +} + +private final class ChannelBoostersContextImpl { + private let queue: Queue + private let account: Account + private let peerId: PeerId + private let disposable = MetaDisposable() + private let updateDisposables = DisposableSet() + private var isLoadingMore: Bool = false + private var hasLoadedOnce: Bool = false + private var canLoadMore: Bool = true + private var loadedFromCache = false + private var results: [ChannelBoostersContext.State.Booster] = [] + private var count: Int32 + private var lastOffset: String? + private var populateCache: Bool = true + + let state = Promise() + + init(queue: Queue, account: Account, peerId: PeerId) { + self.queue = queue + self.account = account + self.peerId = peerId + + self.count = 0 + + self.isLoadingMore = true + self.disposable.set((account.postbox.transaction { transaction -> (peers: [ChannelBoostersContext.State.Booster], count: Int32, canLoadMore: Bool)? in + let cachedResult = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosters, key: CachedChannelBoosters.key(peerId: peerId)))?.get(CachedChannelBoosters.self) + if let cachedResult = cachedResult { + var result: [ChannelBoostersContext.State.Booster] = [] + for peerId in cachedResult.peerIds { + if let peer = transaction.getPeer(peerId), let expires = cachedResult.dates[peerId] { + result.append(ChannelBoostersContext.State.Booster(peer: EnginePeer(peer), expires: expires)) + } else { + return nil + } + } + return (result, cachedResult.count, true) + } else { + return nil + } + } + |> deliverOn(self.queue)).start(next: { [weak self] cachedPeersCountAndCanLoadMore in + guard let strongSelf = self else { + return + } + strongSelf.isLoadingMore = false + if let (cachedPeers, cachedCount, canLoadMore) = cachedPeersCountAndCanLoadMore { + strongSelf.results = cachedPeers + strongSelf.count = cachedCount + strongSelf.hasLoadedOnce = true + strongSelf.canLoadMore = canLoadMore + strongSelf.loadedFromCache = true + } + strongSelf.loadMore() + })) + + self.loadMore() + } + + deinit { + self.disposable.dispose() + } + + func reload() { + self.loadedFromCache = true + self.populateCache = true + self.loadMore() + } + + func loadMore() { + if self.isLoadingMore { + return + } + self.isLoadingMore = true + let account = self.account + let accountPeerId = account.peerId + let peerId = self.peerId + let populateCache = self.populateCache + + if self.loadedFromCache { + self.loadedFromCache = false + } + let lastOffset = self.lastOffset + + self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal<([ChannelBoostersContext.State.Booster], Int32, String?), NoError> in + if let inputPeer = inputPeer { + let offset = lastOffset ?? "" + let limit: Int32 = lastOffset == nil ? 25 : 50 + + let signal = account.network.request(Api.functions.stories.getBoostersList(peer: inputPeer, offset: offset, limit: limit)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<([ChannelBoostersContext.State.Booster], Int32, String?), NoError> in + return account.postbox.transaction { transaction -> ([ChannelBoostersContext.State.Booster], Int32, String?) in + guard let result = result else { + return ([], 0, nil) + } + switch result { + case let .boostersList(_, count, boosters, nextOffset, users): + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) + var resultBoosters: [ChannelBoostersContext.State.Booster] = [] + for booster in boosters { + let peerId: EnginePeer.Id + let expires: Int32 + switch booster { + case let .booster(userId, expiresValue): + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + expires = expiresValue + } + if let peer = transaction.getPeer(peerId) { + resultBoosters.append(ChannelBoostersContext.State.Booster(peer: EnginePeer(peer), expires: expires)) + } + } + if populateCache { + if let entry = CodableEntry(CachedChannelBoosters(boosters: resultBoosters, count: count)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosters, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry) + } + } + return (resultBoosters, count, nextOffset) + } + } + } + return signal + } else { + return .single(([], 0, nil)) + } + } + |> deliverOn(self.queue)).start(next: { [weak self] boosters, updatedCount, nextOffset in + guard let strongSelf = self else { + return + } + strongSelf.lastOffset = nextOffset + if strongSelf.populateCache { + strongSelf.populateCache = false + strongSelf.results.removeAll() + } + var existingIds = Set(strongSelf.results.map { $0.peer.id }) + for booster in boosters { + if !existingIds.contains(booster.peer.id) { + strongSelf.results.append(booster) + existingIds.insert(booster.peer.id) + } + } + strongSelf.isLoadingMore = false + strongSelf.hasLoadedOnce = true + strongSelf.canLoadMore = !boosters.isEmpty + if strongSelf.canLoadMore { + strongSelf.count = max(updatedCount, Int32(strongSelf.results.count)) + } else { + strongSelf.count = Int32(strongSelf.results.count) + } + strongSelf.updateState() + })) + self.updateState() + } + + private func updateCache() { + guard self.hasLoadedOnce && !self.isLoadingMore else { + return + } + + let peerId = self.peerId + let resultBoosters = Array(self.results.prefix(50)) + let count = self.count + self.updateDisposables.add(self.account.postbox.transaction({ transaction in + if let entry = CodableEntry(CachedChannelBoosters(boosters: resultBoosters, count: count)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosters, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry) + } + }).start()) + } + + private func updateState() { + self.state.set(.single(ChannelBoostersContext.State(boosters: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count))) + } +} + +public final class ChannelBoostersContext { + public struct State: Equatable { + public struct Booster: Equatable { + public var peer: EnginePeer + public var expires: Int32 + } + public var boosters: [Booster] + public var isLoadingMore: Bool + public var hasLoadedOnce: Bool + public var canLoadMore: Bool + public var count: Int32 + + public static var Empty = State(boosters: [], isLoadingMore: false, hasLoadedOnce: true, canLoadMore: false, count: 0) + public static var Loading = State(boosters: [], isLoadingMore: false, hasLoadedOnce: false, canLoadMore: false, count: 0) + } + + + private let queue: Queue = Queue() + private let impl: QueueLocalObject + + public var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.state.get().start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + public init(account: Account, peerId: PeerId) { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return ChannelBoostersContextImpl(queue: queue, account: account, peerId: peerId) + }) + } + + public func loadMore() { + self.impl.with { impl in + impl.loadMore() + } + } + + public func reload() { + self.impl.with { impl in + impl.reload() + } + } +} + +private final class CachedChannelBoosters: Codable { + private enum CodingKeys: String, CodingKey { + case peerIds + case expires + case count + } + + private struct DictionaryPair: Codable, Hashable { + var key: Int64 + var value: String + + init(_ key: Int64, value: String) { + self.key = key + self.value = value + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.key = try container.decode(Int64.self, forKey: "k") + self.value = try container.decode(String.self, forKey: "v") + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.key, forKey: "k") + try container.encode(self.value, forKey: "v") + } + } + + let peerIds: [EnginePeer.Id] + let dates: [EnginePeer.Id: Int32] + let count: Int32 + + static func key(peerId: EnginePeer.Id) -> ValueBoxKey { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: peerId.toInt64()) + return key + } + + init(boosters: [ChannelBoostersContext.State.Booster], count: Int32) { + self.peerIds = boosters.map { $0.peer.id } + self.dates = boosters.reduce(into: [EnginePeer.Id: Int32]()) { + $0[$1.peer.id] = $1.expires + } + self.count = count + } + + init(peerIds: [PeerId], dates: [PeerId: Int32], count: Int32) { + self.peerIds = peerIds + self.dates = dates + self.count = count + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.peerIds = (try container.decode([Int64].self, forKey: .peerIds)).map(EnginePeer.Id.init) + + var dates: [EnginePeer.Id: Int32] = [:] + let datesArray = try container.decode([Int64].self, forKey: .expires) + for index in stride(from: 0, to: datesArray.endIndex, by: 2) { + let userId = datesArray[index] + let date = datesArray[index + 1] + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + dates[peerId] = Int32(clamping: date) + } + self.dates = dates + + self.count = try container.decode(Int32.self, forKey: .count) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.peerIds.map { $0.toInt64() }, forKey: .peerIds) + + var dates: [Int64] = [] + for (peerId, date) in self.dates { + dates.append(peerId.id._internalGetInt64Value()) + dates.append(Int64(date)) + } + + try container.encode(dates, forKey: .expires) + try container.encode(self.count, forKey: .count) + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 3269fd80ce..e87c9baea4 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -110,6 +110,7 @@ public struct Namespaces { public static let cachedPeerStoryListHeads: Int8 = 27 public static let displayedStoryNotifications: Int8 = 28 public static let storySendAsPeerIds: Int8 = 29 + public static let cachedChannelBoosters: Int8 = 30 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 0cf19c71a9..fa19c0b2ee 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1196,6 +1196,10 @@ public extension TelegramEngine { public func canApplyChannelBoost(peerId: EnginePeer.Id) -> Signal { return _internal_canApplyChannelBoost(account: self.account, peerId: peerId) } + + public func applyChannelBoost(peerId: EnginePeer.Id) -> Signal { + return _internal_applyChannelBoost(account: self.account, peerId: peerId) + } } }