diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index d205923f67..a36744e986 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -73,6 +73,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1834973166] = { return Api.BaseTheme.parse_baseThemeTinted($0) } dict[-1132882121] = { return Api.Bool.parse_boolFalse($0) } dict[-1720552011] = { return Api.Bool.parse_boolTrue($0) } + dict[245261184] = { return Api.Booster.parse_booster($0) } dict[-1778593322] = { return Api.BotApp.parse_botApp($0) } dict[1571189943] = { return Api.BotApp.parse_botAppNotModified($0) } dict[-1032140601] = { return Api.BotCommand.parse_botCommand($0) } @@ -419,7 +420,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-193992412] = { return Api.InputWebFileLocation.parse_inputWebFileAudioAlbumThumbLocation($0) } dict[-1625153079] = { return Api.InputWebFileLocation.parse_inputWebFileGeoPointLocation($0) } dict[-1036396922] = { return Api.InputWebFileLocation.parse_inputWebFileLocation($0) } - dict[1048946971] = { return Api.Invoice.parse_invoice($0) } + dict[1572428309] = { return Api.Invoice.parse_invoice($0) } dict[-1059185703] = { return Api.JSONObjectValue.parse_jsonObjectValue($0) } dict[-146520221] = { return Api.JSONValue.parse_jsonArray($0) } dict[-952869270] = { return Api.JSONValue.parse_jsonBool($0) } @@ -1175,9 +1176,10 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[276907596] = { return Api.storage.FileType.parse_fileWebp($0) } dict[1862033025] = { return Api.stories.AllStories.parse_allStories($0) } dict[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) } - dict[2061518568] = { return Api.stories.BoostsStatus.parse_boostsStatus($0) } + dict[-203604707] = { return Api.stories.BoostersList.parse_boostersList($0) } + dict[1726619631] = { return Api.stories.BoostsStatus.parse_boostsStatus($0) } dict[-1021889145] = { return Api.stories.CanApplyBoostResult.parse_canApplyBoostOk($0) } - dict[-1532908712] = { return Api.stories.CanApplyBoostResult.parse_canApplyBoostReplace($0) } + dict[1898726997] = { return Api.stories.CanApplyBoostResult.parse_canApplyBoostReplace($0) } dict[-890861720] = { return Api.stories.PeerStories.parse_peerStories($0) } dict[1574486984] = { return Api.stories.Stories.parse_stories($0) } dict[-560009955] = { return Api.stories.StoryViews.parse_storyViews($0) } @@ -1287,6 +1289,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.Bool: _1.serialize(buffer, boxed) + case let _1 as Api.Booster: + _1.serialize(buffer, boxed) case let _1 as Api.BotApp: _1.serialize(buffer, boxed) case let _1 as Api.BotCommand: @@ -2057,6 +2061,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.stories.AllStories: _1.serialize(buffer, boxed) + case let _1 as Api.stories.BoostersList: + _1.serialize(buffer, boxed) case let _1 as Api.stories.BoostsStatus: _1.serialize(buffer, boxed) case let _1 as Api.stories.CanApplyBoostResult: diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index d16e06c361..d6409da385 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -892,6 +892,46 @@ public extension Api { } } +public extension Api { + enum Booster: TypeConstructorDescription { + case booster(userId: Int64, expires: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .booster(let userId, let expires): + if boxed { + buffer.appendInt32(245261184) + } + serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt32(expires, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .booster(let userId, let expires): + return ("booster", [("userId", userId as Any), ("expires", expires as Any)]) + } + } + + public static func parse_booster(_ reader: BufferReader) -> Booster? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.Booster.booster(userId: _1!, expires: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum BotApp: TypeConstructorDescription { case botApp(flags: Int32, id: Int64, accessHash: Int64, shortName: String, title: String, description: String, photo: Api.Photo, document: Api.Document?, hash: Int64) diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 3448343725..714fc85780 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -116,13 +116,13 @@ public extension Api { } public extension Api { enum Invoice: TypeConstructorDescription { - case invoice(flags: Int32, currency: String, prices: [Api.LabeledPrice], maxTipAmount: Int64?, suggestedTipAmounts: [Int64]?, recurringTermsUrl: String?) + case invoice(flags: Int32, currency: String, prices: [Api.LabeledPrice], maxTipAmount: Int64?, suggestedTipAmounts: [Int64]?, termsUrl: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts, let recurringTermsUrl): + case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts, let termsUrl): if boxed { - buffer.appendInt32(1048946971) + buffer.appendInt32(1572428309) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false) @@ -137,15 +137,15 @@ public extension Api { for item in suggestedTipAmounts! { serializeInt64(item, buffer: buffer, boxed: false) }} - if Int(flags) & Int(1 << 9) != 0 {serializeString(recurringTermsUrl!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 10) != 0 {serializeString(termsUrl!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts, let recurringTermsUrl): - return ("invoice", [("flags", flags as Any), ("currency", currency as Any), ("prices", prices as Any), ("maxTipAmount", maxTipAmount as Any), ("suggestedTipAmounts", suggestedTipAmounts as Any), ("recurringTermsUrl", recurringTermsUrl as Any)]) + case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts, let termsUrl): + return ("invoice", [("flags", flags as Any), ("currency", currency as Any), ("prices", prices as Any), ("maxTipAmount", maxTipAmount as Any), ("suggestedTipAmounts", suggestedTipAmounts as Any), ("termsUrl", termsUrl as Any)]) } } @@ -165,15 +165,15 @@ public extension Api { _5 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } } var _6: String? - if Int(_1!) & Int(1 << 9) != 0 {_6 = parseString(reader) } + if Int(_1!) & Int(1 << 10) != 0 {_6 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = (Int(_1!) & Int(1 << 8) == 0) || _4 != nil let _c5 = (Int(_1!) & Int(1 << 8) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 9) == 0) || _6 != nil + let _c6 = (Int(_1!) & Int(1 << 10) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.Invoice.invoice(flags: _1!, currency: _2!, prices: _3!, maxTipAmount: _4, suggestedTipAmounts: _5, recurringTermsUrl: _6) + return Api.Invoice.invoice(flags: _1!, currency: _2!, prices: _3!, maxTipAmount: _4, suggestedTipAmounts: _5, termsUrl: _6) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index bafb096238..b7979ad089 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -281,27 +281,93 @@ public extension Api.stories { } } public extension Api.stories { - enum BoostsStatus: TypeConstructorDescription { - case boostsStatus(flags: Int32, level: Int32, boosts: Int32, nextLevelBoosts: Int32?) + enum BoostersList: TypeConstructorDescription { + case boostersList(flags: Int32, count: Int32, boosters: [Api.Booster], nextOffset: String?, users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .boostsStatus(let flags, let level, let boosts, let nextLevelBoosts): + case .boostersList(let flags, let count, let boosters, let nextOffset, let users): if boxed { - buffer.appendInt32(2061518568) + buffer.appendInt32(-203604707) } serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(level, buffer: buffer, boxed: false) - serializeInt32(boosts, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeInt32(nextLevelBoosts!, buffer: buffer, boxed: false)} + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(boosters.count)) + for item in boosters { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .boostsStatus(let flags, let level, let boosts, let nextLevelBoosts): - return ("boostsStatus", [("flags", flags as Any), ("level", level as Any), ("boosts", boosts as Any), ("nextLevelBoosts", nextLevelBoosts as Any)]) + case .boostersList(let flags, let count, let boosters, let nextOffset, let users): + return ("boostersList", [("flags", flags as Any), ("count", count as Any), ("boosters", boosters as Any), ("nextOffset", nextOffset as Any), ("users", users as Any)]) + } + } + + public static func parse_boostersList(_ reader: BufferReader) -> BoostersList? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.Booster]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Booster.self) + } + var _4: String? + if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.stories.BoostersList.boostersList(flags: _1!, count: _2!, boosters: _3!, nextOffset: _4, users: _5!) + } + else { + return nil + } + } + + } +} +public extension Api.stories { + enum BoostsStatus: TypeConstructorDescription { + case boostsStatus(flags: Int32, level: Int32, currentLevelBoosts: Int32, boosts: Int32, nextLevelBoosts: Int32?, premiumAudience: Api.StatsPercentValue?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let nextLevelBoosts, let premiumAudience): + if boxed { + buffer.appendInt32(1726619631) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(level, buffer: buffer, boxed: false) + serializeInt32(currentLevelBoosts, buffer: buffer, boxed: false) + serializeInt32(boosts, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(nextLevelBoosts!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {premiumAudience!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let nextLevelBoosts, let premiumAudience): + return ("boostsStatus", [("flags", flags as Any), ("level", level as Any), ("currentLevelBoosts", currentLevelBoosts as Any), ("boosts", boosts as Any), ("nextLevelBoosts", nextLevelBoosts as Any), ("premiumAudience", premiumAudience as Any)]) } } @@ -313,13 +379,21 @@ public extension Api.stories { var _3: Int32? _3 = reader.readInt32() var _4: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_4 = reader.readInt32() } + _4 = reader.readInt32() + var _5: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt32() } + var _6: Api.StatsPercentValue? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.StatsPercentValue + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.stories.BoostsStatus.boostsStatus(flags: _1!, level: _2!, boosts: _3!, nextLevelBoosts: _4) + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.stories.BoostsStatus.boostsStatus(flags: _1!, level: _2!, currentLevelBoosts: _3!, boosts: _4!, nextLevelBoosts: _5, premiumAudience: _6) } else { return nil @@ -343,7 +417,7 @@ public extension Api.stories { break case .canApplyBoostReplace(let currentBoost, let chats): if boxed { - buffer.appendInt32(-1532908712) + buffer.appendInt32(1898726997) } currentBoost.serialize(buffer, true) buffer.appendInt32(481674261) diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 70666d4515..19f01cb8d7 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -8664,6 +8664,23 @@ public extension Api.functions.stories { }) } } +public extension Api.functions.stories { + static func getBoostersList(peer: Api.InputPeer, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(863959424) + peer.serialize(buffer, true) + serializeString(offset, buffer: buffer, boxed: false) + serializeInt32(limit, buffer: buffer, boxed: false) + return (FunctionDescription(name: "stories.getBoostersList", parameters: [("peer", String(describing: peer)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.BoostersList? in + let reader = BufferReader(buffer) + var result: Api.stories.BoostersList? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.stories.BoostersList + } + return result + }) + } +} public extension Api.functions.stories { static func getBoostsStatus(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/State/ChannelBoost.swift b/submodules/TelegramCore/Sources/State/ChannelBoost.swift index 80eb5652a2..b09f8f4f2d 100644 --- a/submodules/TelegramCore/Sources/State/ChannelBoost.swift +++ b/submodules/TelegramCore/Sources/State/ChannelBoost.swift @@ -6,12 +6,16 @@ import SwiftSignalKit public final class ChannelBoostStatus: Equatable { public let level: Int public let boosts: Int + public let currentLevelBoosts: Int public let nextLevelBoosts: Int? + public let premiumAudience: StatsPercentValue? - public init(level: Int, boosts: Int, nextLevelBoosts: Int?) { + public init(level: Int, boosts: Int, currentLevelBoosts: Int, nextLevelBoosts: Int?, premiumAudience: StatsPercentValue?) { self.level = level self.boosts = boosts + self.currentLevelBoosts = currentLevelBoosts self.nextLevelBoosts = nextLevelBoosts + self.premiumAudience = premiumAudience } public static func ==(lhs: ChannelBoostStatus, rhs: ChannelBoostStatus) -> Bool { @@ -21,9 +25,15 @@ public final class ChannelBoostStatus: Equatable { if lhs.boosts != rhs.boosts { return false } + if lhs.currentLevelBoosts != rhs.currentLevelBoosts { + return false + } if lhs.nextLevelBoosts != rhs.nextLevelBoosts { return false } + if lhs.premiumAudience != rhs.premiumAudience { + return false + } return true } } @@ -47,8 +57,8 @@ func _internal_getChannelBoostStatus(account: Account, peerId: PeerId) -> Signal } switch result { - case let .boostsStatus(_, level, boosts, nextLevelBoosts): - return ChannelBoostStatus(level: Int(level), boosts: Int(boosts), nextLevelBoosts: nextLevelBoosts.flatMap(Int.init)) + case let .boostsStatus(_, level, currentLevelBoosts, boosts, nextLevelBoosts, premiumAudience): + return ChannelBoostStatus(level: Int(level), boosts: Int(boosts), currentLevelBoosts: Int(currentLevelBoosts), nextLevelBoosts: nextLevelBoosts.flatMap(Int.init), premiumAudience: premiumAudience.flatMap({ StatsPercentValue(apiPercentValue: $0) })) } } } @@ -58,8 +68,9 @@ public enum CanApplyBoostStatus { public enum ErrorReason { case generic case premiumRequired - case floodWait + case floodWait(Int32) case peerBoostAlreadyActive + case giftedPremiumNotAllowed } case ok @@ -84,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 } @@ -115,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) + } } }