From 7d3ab98250443181ebdbae238c404b3587c6d257 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 3 Dec 2024 14:40:26 +0800 Subject: [PATCH] Stars ref --- .../Sources/AccountContext.swift | 6 +- .../Sources/State/AccountStateManager.swift | 21 + .../TelegramEngine/Messages/BotWebView.swift | 566 +++++++++++++----- .../Peers/TelegramEnginePeers.swift | 16 +- .../Sources/AffiliateProgramSetupScreen.swift | 173 +++--- .../Sources/JoinAffiliateProgramScreen.swift | 14 +- 6 files changed, 540 insertions(+), 256 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index da969afdc5..645301d9c9 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -896,10 +896,10 @@ public enum JoinAffiliateProgramScreenMode { public final class Active { public let targetPeer: EnginePeer - public let bot: TelegramConnectedStarRefBotList.Item - public let copyLink: (TelegramConnectedStarRefBotList.Item) -> Void + public let bot: EngineConnectedStarRefBotsContext.Item + public let copyLink: (EngineConnectedStarRefBotsContext.Item) -> Void - public init(targetPeer: EnginePeer, bot: TelegramConnectedStarRefBotList.Item, copyLink: @escaping (TelegramConnectedStarRefBotList.Item) -> Void) { + public init(targetPeer: EnginePeer, bot: EngineConnectedStarRefBotsContext.Item, copyLink: @escaping (EngineConnectedStarRefBotsContext.Item) -> Void) { self.targetPeer = targetPeer self.bot = bot self.copyLink = copyLink diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index ab4ff74eef..1ebfc231be 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -333,6 +333,11 @@ public final class AccountStateManager { return self.sentScheduledMessageIdsPipe.signal() } + fileprivate let starRefBotConnectionEventsPipe = ValuePipe() + public var starRefBotConnectionEvents: Signal { + return self.starRefBotConnectionEventsPipe.signal() + } + private var updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:] private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext() private var updatedRevenueBalancesContext = UpdatedRevenueBalancesSubscriberContext() @@ -1804,6 +1809,10 @@ public final class AccountStateManager { self.removePossiblyDeliveredMessagesUniqueIds.merge(uniqueIds, uniquingKeysWith: { _, rhs in rhs }) } } + + func addStarRefBotConnectionEvent(event: StarRefBotConnectionEvent) { + self.starRefBotConnectionEventsPipe.putNext(event) + } } private let impl: QueueLocalObject @@ -2183,6 +2192,18 @@ public final class AccountStateManager { public func synchronouslyIsMessageDeletedRemotely(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { return self.messagesRemovedContext.synchronouslyIsMessageDeletedRemotely(ids: ids) } + + func starRefBotConnectionEvents() -> Signal { + return self.impl.signalWith { impl, subscriber in + return impl.starRefBotConnectionEventsPipe.signal().start(next: subscriber.putNext) + } + } + + func addStarRefBotConnectionEvent(event: StarRefBotConnectionEvent) { + self.impl.with { impl in + impl.addStarRefBotConnectionEvent(event: event) + } + } } func resolveNotificationSettings(list: [TelegramPeerNotificationSettings], defaultSettings: MessageNotificationSettings) -> (sound: PeerMessageSound, notify: Bool, displayContents: Bool) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index d06d6be08e..a7893ba74d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -601,49 +601,12 @@ public func formatPermille(_ value: Int) -> String { } } -func _internal_updateStarRefProgram(account: Account, id: EnginePeer.Id, program: (commissionPermille: Int32, durationMonths: Int32?)?) -> Signal { - return account.postbox.transaction { transaction -> Api.InputUser? in - return transaction.getPeer(id).flatMap(apiInputUser) - } - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer else { - return .complete() - } - - var flags: Int32 = 0 - if let program, program.durationMonths != nil { - flags |= 1 << 0 - } - - return account.network.request(Api.functions.bots.updateStarRefProgram( - flags: flags, - bot: inputPeer, - commissionPermille: program?.commissionPermille ?? 0, - durationMonths: program?.durationMonths - )) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { result -> Signal in - guard let result else { - return .complete() - } - return account.postbox.transaction { transaction -> Void in - transaction.updatePeerCachedData(peerIds: Set([id]), update: { _, current in - guard var current = current as? CachedUserData else { - return current ?? CachedUserData() - } - current = current.withUpdatedStarRefProgram(TelegramStarRefProgram(apiStarRefProgram: result)) - return current - }) - } - |> ignoreValues - } - } +public enum StarRefBotConnectionEvent { + case add(peerId: EnginePeer.Id, item: EngineConnectedStarRefBotsContext.Item) + case remove(peerId: EnginePeer.Id, url: String) } -public final class TelegramConnectedStarRefBotList : Equatable { +public final class EngineConnectedStarRefBotsContext { public final class Item: Equatable { public let peer: EnginePeer public let url: String @@ -689,84 +652,176 @@ public final class TelegramConnectedStarRefBotList : Equatable { } } - public let items: [Item] - public let totalCount: Int - - public init(items: [Item], totalCount: Int) { - self.items = items - self.totalCount = totalCount - } - - public static func == (lhs: TelegramConnectedStarRefBotList, rhs: TelegramConnectedStarRefBotList) -> Bool { - return lhs.items == rhs.items && lhs.totalCount == rhs.totalCount - } -} - -func _internal_requestConnectedStarRefBots(account: Account, id: EnginePeer.Id, offset: (timestamp: Int32, link: String)?, limit: Int) -> Signal { - return account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(id).flatMap(apiInputPeer) - } - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer else { - return .single(nil) - } - var flags: Int32 = 0 - if offset != nil { - flags |= 1 << 2 - } - return account.network.request(Api.functions.payments.getConnectedStarRefBots( - flags: flags, - peer: inputPeer, - offsetDate: offset?.timestamp, - offsetLink: offset?.link, - limit: Int32(limit) - )) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { result -> Signal in - guard let result else { - return .single(nil) + public struct State: Equatable { + public struct Offset: Equatable { + fileprivate var isInitial: Bool + fileprivate var timestamp: Int32 + fileprivate var link: String + + fileprivate init(isInitial: Bool, timestamp: Int32, link: String) { + self.isInitial = isInitial + self.timestamp = timestamp + self.link = link } - return account.postbox.transaction { transaction -> TelegramConnectedStarRefBotList? in - switch result { - case let .connectedStarRefBots(count, connectedBots, users): - updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users)) - - var items: [TelegramConnectedStarRefBotList.Item] = [] - for connectedBot in connectedBots { - switch connectedBot { - case let .connectedBotStarRef(_, url, date, botId, commissionPermille, durationMonths, participants, revenue): - guard let botPeer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId))) else { - continue - } - items.append(TelegramConnectedStarRefBotList.Item( - peer: EnginePeer(botPeer), - url: url, - timestamp: date, - commissionPermille: commissionPermille, - durationMonths: durationMonths, - participants: participants, - revenue: revenue - )) + } + + public var items: [Item] + public var totalCount: Int + public var nextOffset: Offset? + public var isLoaded: Bool + + public init(items: [Item], totalCount: Int, nextOffset: Offset?, isLoaded: Bool) { + self.items = items + self.totalCount = totalCount + self.nextOffset = nextOffset + self.isLoaded = isLoaded + } + } + + private final class Impl { + let queue: Queue + let account: Account + let peerId: EnginePeer.Id + + var state: State + var pendingRemoveItems = Set() + var statePromise = Promise() + + var loadMoreDisposable: Disposable? + var isLoadingMore: Bool = false + + var eventsDisposable: Disposable? + + init(queue: Queue, account: Account, peerId: EnginePeer.Id) { + self.queue = queue + self.account = account + self.peerId = peerId + + self.state = State(items: [], totalCount: 0, nextOffset: State.Offset(isInitial: true, timestamp: 0, link: ""), isLoaded: false) + self.updateState() + + self.loadMore() + + self.eventsDisposable = (account.stateManager.starRefBotConnectionEvents() + |> deliverOn(self.queue)).startStrict(next: { [weak self] event in + guard let self else { + return + } + switch event { + case let .add(peerId, item): + if peerId == self.peerId { + self.state.items.insert(item, at: 0) + self.updateState() + } + case let .remove(peerId, url): + if peerId == self.peerId { + self.state.items.removeAll(where: { $0.url == url }) + self.updateState() + } + } + }) + } + + deinit { + assert(self.queue.isCurrent()) + self.loadMoreDisposable?.dispose() + self.eventsDisposable?.dispose() + } + + func loadMore() { + if self.isLoadingMore { + return + } + guard let offset = self.state.nextOffset else { + return + } + self.isLoadingMore = true + + var effectiveOffset: (timestamp: Int32, link: String)? + if !offset.isInitial { + effectiveOffset = (timestamp: offset.timestamp, link: offset.link) + } + self.loadMoreDisposable?.dispose() + self.loadMoreDisposable = (_internal_requestConnectedStarRefBots(account: self.account, id: self.peerId, offset: effectiveOffset, limit: 100) + |> deliverOn(self.queue)).startStrict(next: { [weak self] result in + guard let self else { + return + } + + self.isLoadingMore = false + + self.state.isLoaded = true + if let result, !result.items.isEmpty { + for item in result.items { + if !self.state.items.contains(where: { $0.url == item.url }) { + self.state.items.append(item) } } - - return TelegramConnectedStarRefBotList(items: items, totalCount: Int(count)) + if result.nextOffset != nil { + self.state.totalCount = result.totalCount + } else { + self.state.totalCount = self.state.items.count + } + self.state.nextOffset = result.nextOffset.flatMap { value in + return State.Offset(isInitial: false, timestamp: value.timestamp, link: value.link) + } + } else { + self.state.totalCount = self.state.items.count + self.state.nextOffset = nil + } + + self.updateState() + }) + } + + private func updateState() { + var state = self.state + if !self.pendingRemoveItems.isEmpty { + state.items = state.items.filter { item in + return !self.pendingRemoveItems.contains(item.url) } } + self.statePromise.set(.single(state)) + } + + func remove(url: String) { + self.pendingRemoveItems.insert(url) + let _ = _internal_removeConnectedStarRefBot(account: self.account, id: self.peerId, link: url).startStandalone() + self.updateState() + } + } + + private let queue: Queue + private let impl: QueueLocalObject + + public var state: Signal { + return self.impl.signalWith { impl, subscriber in + return impl.statePromise.get().start(next: subscriber.putNext) + } + } + + init(account: Account, peerId: EnginePeer.Id) { + let queue = Queue.mainQueue() + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, account: account, peerId: peerId) + }) + } + + public func loadMore() { + self.impl.with { impl in + impl.loadMore() + } + } + + public func remove(url: String) { + self.impl.with { impl in + impl.remove(url: url) } } } -public final class TelegramSuggestedStarRefBotList: Equatable { - public enum SortMode { - case date - case profitability - case revenue - } - +public final class EngineSuggestedStarRefBotsContext { public final class Item: Equatable { public let peer: EnginePeer public let program: TelegramStarRefProgram @@ -787,26 +842,237 @@ public final class TelegramSuggestedStarRefBotList: Equatable { } } - public let items: [Item] - public let totalCount: Int - public let nextOffset: String? - - public init(items: [Item], totalCount: Int, nextOffset: String?) { - self.items = items - self.totalCount = totalCount - self.nextOffset = nextOffset + public struct State: Equatable { + public var items: [Item] + public var totalCount: Int + public var nextOffset: String? + public var isLoaded: Bool + + public init(items: [Item], totalCount: Int, nextOffset: String?, isLoaded: Bool) { + self.items = items + self.totalCount = totalCount + self.nextOffset = nextOffset + self.isLoaded = isLoaded + } } - public static func == (lhs: TelegramSuggestedStarRefBotList, rhs: TelegramSuggestedStarRefBotList) -> Bool { - return lhs.items == rhs.items && lhs.totalCount == rhs.totalCount && lhs.nextOffset == rhs.nextOffset + public enum SortMode { + case date + case profitability + case revenue + } + + private final class Impl { + let queue: Queue + let account: Account + let peerId: EnginePeer.Id + let sortMode: SortMode + + var state: State + var statePromise = Promise() + + var loadMoreDisposable: Disposable? + var isLoadingMore: Bool = false + + init(queue: Queue, account: Account, peerId: EnginePeer.Id, sortMode: SortMode) { + self.queue = queue + self.account = account + self.peerId = peerId + self.sortMode = sortMode + + self.state = State(items: [], totalCount: 0, nextOffset: "", isLoaded: false) + self.updateState() + + self.loadMore() + } + + deinit { + assert(self.queue.isCurrent()) + self.loadMoreDisposable?.dispose() + } + + func loadMore() { + if self.isLoadingMore { + return + } + guard let offset = self.state.nextOffset else { + return + } + self.isLoadingMore = true + + self.loadMoreDisposable?.dispose() + self.loadMoreDisposable = (_internal_requestSuggestedStarRefBots(account: self.account, id: self.peerId, sortMode: self.sortMode, offset: offset, limit: 100) + |> deliverOn(self.queue)).startStrict(next: { [weak self] result in + guard let self else { + return + } + self.isLoadingMore = false + + self.state.isLoaded = true + if let result, !result.items.isEmpty { + for item in result.items { + if !self.state.items.contains(where: { $0.peer.id == item.peer.id }) { + self.state.items.append(item) + } + } + if result.nextOffset != nil { + self.state.totalCount = result.totalCount + } else { + self.state.totalCount = self.state.items.count + } + self.state.nextOffset = result.nextOffset + } else { + self.state.totalCount = self.state.items.count + self.state.nextOffset = nil + } + + self.updateState() + }) + } + + private func updateState() { + self.statePromise.set(.single(self.state)) + } + } + + private let queue: Queue + public let sortMode: SortMode + private let impl: QueueLocalObject + + public var state: Signal { + return self.impl.signalWith { impl, subscriber in + return impl.statePromise.get().start(next: subscriber.putNext) + } + } + + init(account: Account, peerId: EnginePeer.Id, sortMode: SortMode) { + let queue = Queue.mainQueue() + self.queue = queue + self.sortMode = sortMode + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, account: account, peerId: peerId, sortMode: sortMode) + }) + } + + public func loadMore() { + self.impl.with { impl in + impl.loadMore() + } } } -func _internal_requestSuggestedStarRefBots(account: Account, id: EnginePeer.Id, sortMode: TelegramSuggestedStarRefBotList.SortMode, offset: String?, limit: Int) -> Signal { +func _internal_updateStarRefProgram(account: Account, id: EnginePeer.Id, program: (commissionPermille: Int32, durationMonths: Int32?)?) -> Signal { + return account.postbox.transaction { transaction -> Api.InputUser? in + return transaction.getPeer(id).flatMap(apiInputUser) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .complete() + } + + var flags: Int32 = 0 + if let program, program.durationMonths != nil { + flags |= 1 << 0 + } + + return account.network.request(Api.functions.bots.updateStarRefProgram( + flags: flags, + bot: inputPeer, + commissionPermille: program?.commissionPermille ?? 0, + durationMonths: program?.durationMonths + )) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .complete() + } + return account.postbox.transaction { transaction -> Void in + transaction.updatePeerCachedData(peerIds: Set([id]), update: { _, current in + guard var current = current as? CachedUserData else { + return current ?? CachedUserData() + } + current = current.withUpdatedStarRefProgram(TelegramStarRefProgram(apiStarRefProgram: result)) + return current + }) + } + |> ignoreValues + } + } +} + +fileprivate func _internal_requestConnectedStarRefBots(account: Account, id: EnginePeer.Id, offset: (timestamp: Int32, link: String)?, limit: Int) -> Signal<(items: [EngineConnectedStarRefBotsContext.Item], totalCount: Int, nextOffset: (timestamp: Int32, link: String)?)?, NoError> { return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(id).flatMap(apiInputPeer) } - |> mapToSignal { inputPeer -> Signal in + |> mapToSignal { inputPeer -> Signal<(items: [EngineConnectedStarRefBotsContext.Item], totalCount: Int, nextOffset: (timestamp: Int32, link: String)?)?, NoError> in + guard let inputPeer else { + return .single(nil) + } + var flags: Int32 = 0 + if offset != nil { + flags |= 1 << 2 + } + return account.network.request(Api.functions.payments.getConnectedStarRefBots( + flags: flags, + peer: inputPeer, + offsetDate: offset?.timestamp, + offsetLink: offset?.link, + limit: Int32(limit) + )) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<(items: [EngineConnectedStarRefBotsContext.Item], totalCount: Int, nextOffset: (timestamp: Int32, link: String)?)?, NoError> in + guard let result else { + return .single(nil) + } + return account.postbox.transaction { transaction -> (items: [EngineConnectedStarRefBotsContext.Item], totalCount: Int, nextOffset: (timestamp: Int32, link: String)?)? in + switch result { + case let .connectedStarRefBots(count, connectedBots, users): + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users)) + + var items: [EngineConnectedStarRefBotsContext.Item] = [] + for connectedBot in connectedBots { + switch connectedBot { + case let .connectedBotStarRef(_, url, date, botId, commissionPermille, durationMonths, participants, revenue): + guard let botPeer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId))) else { + continue + } + items.append(EngineConnectedStarRefBotsContext.Item( + peer: EnginePeer(botPeer), + url: url, + timestamp: date, + commissionPermille: commissionPermille, + durationMonths: durationMonths, + participants: participants, + revenue: revenue + )) + } + } + + var nextOffset: (timestamp: Int32, link: String)? + if !connectedBots.isEmpty { + nextOffset = items.last.flatMap { item in + return (item.timestamp, item.url) + } + } + + return (items: items, totalCount: Int(count), nextOffset: nextOffset) + } + } + } + } +} + +fileprivate func _internal_requestSuggestedStarRefBots(account: Account, id: EnginePeer.Id, sortMode: EngineSuggestedStarRefBotsContext.SortMode, offset: String?, limit: Int) -> Signal<(items: [EngineSuggestedStarRefBotsContext.Item], totalCount: Int, nextOffset: String?)?, NoError> { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(id).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal<(items: [EngineSuggestedStarRefBotsContext.Item], totalCount: Int, nextOffset: String?)?, NoError> in guard let inputPeer else { return .single(nil) } @@ -829,28 +1095,28 @@ func _internal_requestSuggestedStarRefBots(account: Account, id: EnginePeer.Id, |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal<(items: [EngineSuggestedStarRefBotsContext.Item], totalCount: Int, nextOffset: String?)?, NoError> in guard let result else { return .single(nil) } - return account.postbox.transaction { transaction -> TelegramSuggestedStarRefBotList? in + return account.postbox.transaction { transaction -> (items: [EngineSuggestedStarRefBotsContext.Item], totalCount: Int, nextOffset: String?)? in switch result { case let .suggestedStarRefBots(_, count, suggestedBots, users, nextOffset): updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users)) - var items: [TelegramSuggestedStarRefBotList.Item] = [] + var items: [EngineSuggestedStarRefBotsContext.Item] = [] for starRefProgram in suggestedBots { let parsedProgram = TelegramStarRefProgram(apiStarRefProgram: starRefProgram) guard let botPeer = transaction.getPeer(parsedProgram.botId) else { continue } - items.append(TelegramSuggestedStarRefBotList.Item( + items.append(EngineSuggestedStarRefBotsContext.Item( peer: EnginePeer(botPeer), program: parsedProgram )) } - return TelegramSuggestedStarRefBotList(items: items, totalCount: Int(count), nextOffset: nextOffset) + return (items: items, totalCount: Int(count), nextOffset: nextOffset) } } } @@ -861,7 +1127,7 @@ public enum ConnectStarRefBotError { case generic } -func _internal_connectStarRefBot(account: Account, id: EnginePeer.Id, botId: EnginePeer.Id) -> Signal { +func _internal_connectStarRefBot(account: Account, id: EnginePeer.Id, botId: EnginePeer.Id) -> Signal { return account.postbox.transaction { transaction -> (Api.InputPeer?, Api.InputUser?) in return ( transaction.getPeer(id).flatMap(apiInputPeer), @@ -869,7 +1135,7 @@ func _internal_connectStarRefBot(account: Account, id: EnginePeer.Id, botId: Eng ) } |> castError(ConnectStarRefBotError.self) - |> mapToSignal { inputPeer, inputBotUser -> Signal in + |> mapToSignal { inputPeer, inputBotUser -> Signal in guard let inputPeer, let inputBotUser else { return .fail(.generic) } @@ -877,8 +1143,8 @@ func _internal_connectStarRefBot(account: Account, id: EnginePeer.Id, botId: Eng |> mapError { _ -> ConnectStarRefBotError in return .generic } - |> mapToSignal { result -> Signal in - return account.postbox.transaction { transaction -> TelegramConnectedStarRefBotList.Item? in + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> EngineConnectedStarRefBotsContext.Item? in switch result { case let .connectedStarRefBots(_, connectedBots, users): updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users)) @@ -889,7 +1155,7 @@ func _internal_connectStarRefBot(account: Account, id: EnginePeer.Id, botId: Eng guard let botPeer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId))) else { return nil } - return TelegramConnectedStarRefBotList.Item( + return EngineConnectedStarRefBotsContext.Item( peer: EnginePeer(botPeer), url: url, timestamp: date, @@ -905,8 +1171,9 @@ func _internal_connectStarRefBot(account: Account, id: EnginePeer.Id, botId: Eng } } |> castError(ConnectStarRefBotError.self) - |> mapToSignal { item -> Signal in + |> mapToSignal { item -> Signal in if let item { + account.stateManager.addStarRefBotConnectionEvent(event: .add(peerId: id, item: item)) return .single(item) } else { return .fail(.generic) @@ -916,7 +1183,7 @@ func _internal_connectStarRefBot(account: Account, id: EnginePeer.Id, botId: Eng } } -func _internal_removeConnectedStarRefBot(account: Account, id: EnginePeer.Id, link: String) -> Signal { +fileprivate func _internal_removeConnectedStarRefBot(account: Account, id: EnginePeer.Id, link: String) -> Signal { return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(id).flatMap(apiInputPeer) } @@ -938,26 +1205,9 @@ func _internal_removeConnectedStarRefBot(account: Account, id: EnginePeer.Id, li updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users)) let _ = connectedBots - /*if let bot = connectedBots.first { - switch bot { - case let .connectedBotStarRef(_, url, date, botId, commissionPermille, durationMonths, participants, revenue): - guard let botPeer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId))) else { - return - } - return TelegramConnectedStarRefBotList.Item( - peer: EnginePeer(botPeer), - url: url, - timestamp: date, - commissionPermille: commissionPermille, - durationMonths: durationMonths, - participants: participants, - revenue: revenue - ) - } - } else { - return nil - }*/ } + + account.stateManager.addStarRefBotConnectionEvent(event: .remove(peerId: id, url: link)) } |> castError(ConnectStarRefBotError.self) |> ignoreValues @@ -965,14 +1215,14 @@ func _internal_removeConnectedStarRefBot(account: Account, id: EnginePeer.Id, li } } -func _internal_getStarRefBotConnection(account: Account, id: EnginePeer.Id, targetId: EnginePeer.Id) -> Signal { +func _internal_getStarRefBotConnection(account: Account, id: EnginePeer.Id, targetId: EnginePeer.Id) -> Signal { return account.postbox.transaction { transaction -> (Api.InputUser?, Api.InputPeer?) in return ( transaction.getPeer(id).flatMap(apiInputUser), transaction.getPeer(targetId).flatMap(apiInputPeer) ) } - |> mapToSignal { inputPeer, targetPeer -> Signal in + |> mapToSignal { inputPeer, targetPeer -> Signal in guard let inputPeer, let targetPeer else { return .single(nil) } @@ -981,11 +1231,11 @@ func _internal_getStarRefBotConnection(account: Account, id: EnginePeer.Id, targ |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal in guard let result else { return .single(nil) } - return account.postbox.transaction { transaction -> TelegramConnectedStarRefBotList.Item? in + return account.postbox.transaction { transaction -> EngineConnectedStarRefBotsContext.Item? in switch result { case let .connectedStarRefBots(_, connectedBots, users): updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users)) @@ -1001,7 +1251,7 @@ func _internal_getStarRefBotConnection(account: Account, id: EnginePeer.Id, targ guard let botPeer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId))) else { return nil } - return TelegramConnectedStarRefBotList.Item( + return EngineConnectedStarRefBotsContext.Item( peer: EnginePeer(botPeer), url: url, timestamp: date, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index b5f66d6f2b..6dbf91c823 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1637,23 +1637,19 @@ public extension TelegramEngine { return _internal_updateStarRefProgram(account: self.account, id: id, program: program) } - public func requestConnectedStarRefBots(id: EnginePeer.Id, offset: (timestamp: Int32, link: String)?, limit: Int) -> Signal { - return _internal_requestConnectedStarRefBots(account: self.account, id: id, offset: offset, limit: limit) + public func connectedStarRefBots(id: EnginePeer.Id) -> EngineConnectedStarRefBotsContext { + return EngineConnectedStarRefBotsContext(account: self.account, peerId: id) } - public func requestSuggestedStarRefBots(id: EnginePeer.Id, sortMode: TelegramSuggestedStarRefBotList.SortMode, offset: String?, limit: Int) -> Signal { - return _internal_requestSuggestedStarRefBots(account: self.account, id: id, sortMode: sortMode, offset: offset, limit: limit) + public func suggestedStarRefBots(id: EnginePeer.Id, sortMode: EngineSuggestedStarRefBotsContext.SortMode) -> EngineSuggestedStarRefBotsContext { + return EngineSuggestedStarRefBotsContext(account: self.account, peerId: id, sortMode: sortMode) } - public func connectStarRefBot(id: EnginePeer.Id, botId: EnginePeer.Id) -> Signal { + public func connectStarRefBot(id: EnginePeer.Id, botId: EnginePeer.Id) -> Signal { return _internal_connectStarRefBot(account: self.account, id: id, botId: botId) } - public func removeConnectedStarRefBot(id: EnginePeer.Id, link: String) -> Signal { - return _internal_removeConnectedStarRefBot(account: self.account, id: id, link: link) - } - - public func getStarRefBotConnection(id: EnginePeer.Id, targetId: EnginePeer.Id) -> Signal { + public func getStarRefBotConnection(id: EnginePeer.Id, targetId: EnginePeer.Id) -> Signal { return _internal_getStarRefBotConnection(account: self.account, id: id, targetId: targetId) } diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift index 6ede3555f4..c2d832bc79 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift @@ -127,18 +127,23 @@ final class AffiliateProgramSetupScreenComponent: Component { private var durationValue: Int = 0 private var durationMinValue: Int = 0 + private var ignoreScrolling: Bool = false + private var isApplying: Bool = false private var applyDisposable: Disposable? private var currentProgram: TelegramStarRefProgram? private var programEndTimer: Foundation.Timer? - private var connectedStarBotList: TelegramConnectedStarRefBotList? - private var connectedStarBotListDisposable: Disposable? + private var connectedStarBots: EngineConnectedStarRefBotsContext? + private var connectedStarBotsState: EngineConnectedStarRefBotsContext.State? + private var connectedStarBotsStateDisposable: Disposable? + private var expectedManualRemoveConnectedBotUrl: String? + + private var suggestedStarBots: EngineSuggestedStarRefBotsContext? + private var suggestedStarBotsState: EngineSuggestedStarRefBotsContext.State? + private var suggestedStarBotsStateDisposable: Disposable? - private var suggestedStarBotList: TelegramSuggestedStarRefBotList? - private var suggestedStarBotListDisposable: Disposable? - private var suggestedSortMode: TelegramSuggestedStarRefBotList.SortMode = .profitability private var isSuggestedSortModeUpdating: Bool = false override init(frame: CGRect) { @@ -169,8 +174,8 @@ final class AffiliateProgramSetupScreenComponent: Component { deinit { self.applyDisposable?.dispose() self.programEndTimer?.invalidate() - self.connectedStarBotListDisposable?.dispose() - self.suggestedStarBotListDisposable?.dispose() + self.connectedStarBotsStateDisposable?.dispose() + self.suggestedStarBotsStateDisposable?.dispose() } func scrollToTop() { @@ -182,7 +187,9 @@ final class AffiliateProgramSetupScreenComponent: Component { } func scrollViewDidScroll(_ scrollView: UIScrollView) { - self.updateScrolling(transition: .immediate) + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } } private func requestApplyProgram() { @@ -353,9 +360,27 @@ final class AffiliateProgramSetupScreenComponent: Component { alphaTransition.setAlpha(view: bottomPanelBackgroundView, alpha: bottomPanelAlpha) alphaTransition.setAlpha(layer: self.bottomPanelSeparator, alpha: bottomPanelAlpha) } + + if self.scrollView.bounds.maxY >= self.scrollView.contentSize.height - 100.0 { + var shouldLoadMoreConnected = false + if let connectedStarBotsState = self.connectedStarBotsState, connectedStarBotsState.isLoaded, connectedStarBotsState.nextOffset != nil { + shouldLoadMoreConnected = true + } + + var shouldLoadMoreSuggested = false + if let suggestedStarBotsState = self.suggestedStarBotsState, suggestedStarBotsState.isLoaded, suggestedStarBotsState.nextOffset != nil { + shouldLoadMoreSuggested = true + } + + if shouldLoadMoreConnected { + self.connectedStarBots?.loadMore() + } else if shouldLoadMoreSuggested { + self.suggestedStarBots?.loadMore() + } + } } - private func openConnectedBot(bot: TelegramConnectedStarRefBotList.Item) { + private func openConnectedBot(bot: EngineConnectedStarRefBotsContext.Item) { guard let component = self.component else { return } @@ -393,44 +418,28 @@ final class AffiliateProgramSetupScreenComponent: Component { }) } - private func leaveProgram(bot: TelegramConnectedStarRefBotList.Item) { - guard let component = self.component else { - return - } - - let _ = (component.context.engine.peers.removeConnectedStarRefBot(id: component.initialContent.peerId, link: bot.url) - |> deliverOnMainQueue).startStandalone(completed: { [weak self] in - guard let self else { - return - } - if let connectedStarBotList = self.connectedStarBotList { - var updatedItems = connectedStarBotList.items - if let index = updatedItems.firstIndex(where: { $0.peer.id == bot.peer.id }) { - updatedItems.remove(at: index) - } - self.connectedStarBotList = TelegramConnectedStarRefBotList( - items: updatedItems, - totalCount: connectedStarBotList.totalCount + 1 - ) - self.state?.updated(transition: .immediate) - } - }) + private func leaveProgram(bot: EngineConnectedStarRefBotsContext.Item) { + self.expectedManualRemoveConnectedBotUrl = bot.url + self.connectedStarBots?.remove(url: bot.url) } private func openSortModeMenu(sourceView: UIView) { guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { return } + guard let suggestedStarBots = self.suggestedStarBots else { + return + } var items: [ContextMenuItem] = [] - let availableModes: [(TelegramSuggestedStarRefBotList.SortMode, String)] = [ + let availableModes: [(EngineSuggestedStarRefBotsContext.SortMode, String)] = [ (.profitability, environment.strings.AffiliateProgram_SortSelectorProfitability), (.revenue, environment.strings.AffiliateProgram_SortSelectorRevenue), (.date, environment.strings.AffiliateProgram_SortSelectorDate) ] for (mode, title) in availableModes { - let isSelected = mode == self.suggestedSortMode + let isSelected = mode == suggestedStarBots.sortMode items.append(.action(ContextMenuActionItem(text: title, icon: { theme in if isSelected { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) @@ -443,23 +452,20 @@ final class AffiliateProgramSetupScreenComponent: Component { guard let self else { return } - if self.suggestedSortMode != mode { - self.suggestedSortMode = mode + if self.suggestedStarBots?.sortMode != mode { self.isSuggestedSortModeUpdating = true self.state?.updated(transition: .immediate) - self.suggestedStarBotListDisposable?.dispose() - self.suggestedStarBotListDisposable = (component.context.engine.peers.requestSuggestedStarRefBots( - id: component.initialContent.peerId, - sortMode: self.suggestedSortMode, - offset: nil, - limit: 100) - |> deliverOnMainQueue).startStrict(next: { [weak self] list in + let suggestedStarBots = component.context.engine.peers.suggestedStarRefBots(id: component.initialContent.peerId, sortMode: mode) + self.suggestedStarBots = suggestedStarBots + self.suggestedStarBotsStateDisposable?.dispose() + self.suggestedStarBotsStateDisposable = (suggestedStarBots.state + |> deliverOnMainQueue).startStrict(next: { [weak self] state in guard let self else { return } - self.suggestedStarBotList = list self.isSuggestedSortModeUpdating = false + self.suggestedStarBotsState = state self.state?.updated(transition: .immediate) }) } @@ -491,6 +497,11 @@ final class AffiliateProgramSetupScreenComponent: Component { self.isUpdating = false } + var transition = transition + if !self.scrollView.isDecelerating && !self.scrollView.isDragging { + transition = transition.withUserData(PeerBadgeAvatarComponent.SynchronousLoadHint()) + } + let durationItems: [Int32] = [ 1, 3, @@ -576,28 +587,35 @@ final class AffiliateProgramSetupScreenComponent: Component { self.durationMinValue = 0 } case .connectedPrograms: - self.connectedStarBotListDisposable = (component.context.engine.peers.requestConnectedStarRefBots( - id: component.initialContent.peerId, - offset: nil, - limit: 100) - |> deliverOnMainQueue).startStrict(next: { [weak self] list in + let connectedStarBots = component.context.engine.peers.connectedStarRefBots(id: component.initialContent.peerId) + self.connectedStarBots = connectedStarBots + self.connectedStarBotsStateDisposable = (connectedStarBots.state + |> deliverOnMainQueue).startStrict(next: { [weak self] state in guard let self else { return } - self.connectedStarBotList = list - self.state?.updated(transition: .immediate) + var transition: ComponentTransition = .immediate + if let expectedManualRemoveConnectedBotUrl = self.expectedManualRemoveConnectedBotUrl { + self.expectedManualRemoveConnectedBotUrl = nil + + if let currentState = self.connectedStarBotsState { + if currentState.items.count == state.items.count + 1 && currentState.items.contains(where: { $0.url == expectedManualRemoveConnectedBotUrl }) && !state.items.contains(where: { $0.url == expectedManualRemoveConnectedBotUrl }) { + transition = .easeInOut(duration: 0.25) + } + } + } + self.connectedStarBotsState = state + self.state?.updated(transition: transition) }) - self.suggestedStarBotListDisposable = (component.context.engine.peers.requestSuggestedStarRefBots( - id: component.initialContent.peerId, - sortMode: self.suggestedSortMode, - offset: nil, - limit: 100) - |> deliverOnMainQueue).startStrict(next: { [weak self] list in + let suggestedStarBots = component.context.engine.peers.suggestedStarRefBots(id: component.initialContent.peerId, sortMode: .profitability) + self.suggestedStarBots = suggestedStarBots + self.suggestedStarBotsStateDisposable = (suggestedStarBots.state + |> deliverOnMainQueue).startStrict(next: { [weak self] state in guard let self else { return } - self.suggestedStarBotList = list + self.suggestedStarBotsState = state self.state?.updated(transition: .immediate) }) } @@ -1187,12 +1205,12 @@ final class AffiliateProgramSetupScreenComponent: Component { contentHeight += bottomPanelFrame.height case .connectedPrograms: - if let connectedStarBotList = self.connectedStarBotList, let suggestedStarBotList = self.suggestedStarBotList { - let suggestedStarBotListItems = suggestedStarBotList.items.filter({ item in !connectedStarBotList.items.contains(where: { $0.peer.id == item.peer.id }) }) + if let connectedStarBotsState = self.connectedStarBotsState, connectedStarBotsState.isLoaded, let suggestedStarBots = self.suggestedStarBots, let suggestedStarBotsState = self.suggestedStarBotsState, suggestedStarBotsState.isLoaded { + let suggestedStarBotListItems = suggestedStarBotsState.items.filter({ item in !connectedStarBotsState.items.contains(where: { $0.peer.id == item.peer.id }) }) do { var activeSectionItems: [AnyComponentWithIdentity] = [] - for item in connectedStarBotList.items { + for item in connectedStarBotsState.items { let durationTitle: String if let durationMonths = item.durationMonths { durationTitle = timeIntervalString(strings: environment.strings, value: durationMonths * (30 * 24 * 60 * 60)) @@ -1362,13 +1380,9 @@ final class AffiliateProgramSetupScreenComponent: Component { self.scrollView.addSubview(activeProgramsSectionView) } transition.setFrame(view: activeProgramsSectionView, frame: activeProgramsSectionFrame) - if let connectedStarBotList = self.connectedStarBotList, !connectedStarBotList.items.isEmpty { - activeProgramsSectionView.isHidden = false - } else { - activeProgramsSectionView.isHidden = true - } + transition.setAlpha(view: activeProgramsSectionView, alpha: connectedStarBotsState.items.isEmpty ? 0.0 : 1.0) } - if let connectedStarBotList = self.connectedStarBotList, !connectedStarBotList.items.isEmpty { + if !connectedStarBotsState.items.isEmpty { contentHeight += activeProgramsSectionSize.height contentHeight += sectionSpacing } @@ -1454,24 +1468,13 @@ final class AffiliateProgramSetupScreenComponent: Component { guard let self, let component = self.component else { return } + let _ = (component.context.engine.peers.connectStarRefBot(id: component.initialContent.peerId, botId: peer.id) |> deliverOnMainQueue).startStandalone(next: { [weak self] result in guard let self else { return } - if let connectedStarBotList = self.connectedStarBotList { - var updatedItems = connectedStarBotList.items - if !updatedItems.contains(where: { $0.peer.id == peer.id }) { - updatedItems.insert(result, at: 0) - } - self.connectedStarBotList = TelegramConnectedStarRefBotList( - items: updatedItems, - totalCount: connectedStarBotList.totalCount + 1 - ) - self.state?.updated(transition: .immediate) - - self.openConnectedBot(bot: result) - } + self.openConnectedBot(bot: result) }) } )) @@ -1496,7 +1499,7 @@ final class AffiliateProgramSetupScreenComponent: Component { suggestedHeaderItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(BotSectionSortButtonComponent( theme: environment.theme, strings: environment.strings, - sortMode: self.suggestedSortMode, + sortMode: suggestedStarBots.sortMode, action: { [weak self] sourceView in guard let self else { return @@ -1526,15 +1529,20 @@ final class AffiliateProgramSetupScreenComponent: Component { } transition.setFrame(view: suggestedProgramsSectionView, frame: suggestedProgramsSectionFrame) + suggestedProgramsSectionView.isHidden = connectedStarBotsState.nextOffset != nil + suggestedProgramsSectionView.contentViewImpl.alpha = self.isSuggestedSortModeUpdating ? 0.6 : 1.0 suggestedProgramsSectionView.contentViewImpl.isUserInteractionEnabled = !self.isSuggestedSortModeUpdating } - contentHeight += suggestedProgramsSectionSize.height - contentHeight += sectionSpacing + if connectedStarBotsState.nextOffset == nil { + contentHeight += suggestedProgramsSectionSize.height + contentHeight += sectionSpacing + } } } } + self.ignoreScrolling = true let contentSize = CGSize(width: availableSize.width, height: contentHeight) if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) @@ -1546,6 +1554,7 @@ final class AffiliateProgramSetupScreenComponent: Component { if self.scrollView.scrollIndicatorInsets != scrollInsets { self.scrollView.scrollIndicatorInsets = scrollInsets } + self.ignoreScrolling = false self.updateScrolling(transition: transition) diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift index 688d6544e4..3df171977a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift @@ -1863,13 +1863,13 @@ final class AffiliatePeerSubtitleComponent: Component { final class BotSectionSortButtonComponent: Component { let theme: PresentationTheme let strings: PresentationStrings - let sortMode: TelegramSuggestedStarRefBotList.SortMode + let sortMode: EngineSuggestedStarRefBotsContext.SortMode let action: (UIView) -> Void init( theme: PresentationTheme, strings: PresentationStrings, - sortMode: TelegramSuggestedStarRefBotList.SortMode, + sortMode: EngineSuggestedStarRefBotsContext.SortMode, action: @escaping (UIView) -> Void ) { self.theme = theme @@ -1990,6 +1990,9 @@ final class BotSectionSortButtonComponent: Component { } final class PeerBadgeAvatarComponent: Component { + final class SynchronousLoadHint { + } + let context: AccountContext let peer: EnginePeer let theme: PresentationTheme @@ -2042,6 +2045,11 @@ final class PeerBadgeAvatarComponent: Component { self.component = component self.state = state + var synchronousLoad = false + if transition.userData(SynchronousLoadHint.self) != nil { + synchronousLoad = true + } + let size = CGSize(width: 40.0, height: 40.0) let badgeSize: CGFloat = 18.0 @@ -2062,7 +2070,7 @@ final class PeerBadgeAvatarComponent: Component { context: component.context, theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, peer: component.peer, - synchronousLoad: false, + synchronousLoad: synchronousLoad, displayDimensions: size, cutoutRect: component.hasBadge ? badgeFrame.insetBy(dx: -(1.0 + UIScreenPixel), dy: -(1.0 + UIScreenPixel)) : nil )