import Foundation import TelegramCore import Postbox import SwiftSignalKit private let initialBatchSize: Int32 = 64 private let defaultEmptyTimeout: Double = 2.0 * 60.0 private let headUpdateTimeout: Double = 30.0 private let requestBatchSize: Int32 = 64 enum ChannelMemberListLoadingState: Equatable { case loading(initial: Bool) case ready(hasMore: Bool) } extension ChannelParticipant { var adminInfo: ChannelParticipantAdminInfo? { switch self { case .creator: return nil case let .member(_, _, adminInfo, _): return adminInfo } } var banInfo: ChannelParticipantBannedInfo? { switch self { case .creator: return nil case let .member(_, _, _, banInfo): return banInfo } } } struct ChannelMemberListState { let list: [RenderedChannelParticipant] let loadingState: ChannelMemberListLoadingState func withUpdatedList(_ list: [RenderedChannelParticipant]) -> ChannelMemberListState { return ChannelMemberListState(list: list, loadingState: self.loadingState) } func withUpdatedLoadingState(_ loadingState: ChannelMemberListLoadingState) -> ChannelMemberListState { return ChannelMemberListState(list: self.list, loadingState: loadingState) } } enum ChannelMemberListCategory { case recent case recentSearch(String) case admins(String?) case restricted(String?) case banned(String?) } private protocol ChannelMemberCategoryListContext { var listStateValue: ChannelMemberListState { get } var listState: Signal { get } func loadMore() func reset(_ force: Bool) func replayUpdates(_ updates: [(ChannelParticipant?, RenderedChannelParticipant?, Bool?)]) func forceUpdateHead() } private func isParticipantMember(_ participant: ChannelParticipant, infoIsMember: Bool?) -> Bool { if let banInfo = participant.banInfo { return !banInfo.rights.flags.contains(.banReadMessages) && banInfo.isMember } else if let infoIsMember = infoIsMember { return infoIsMember } else { return true } } private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategoryListContext { private let postbox: Postbox private let network: Network private let accountPeerId: PeerId private let peerId: PeerId private let category: ChannelMemberListCategory var listStateValue: ChannelMemberListState { didSet { self.listStatePromise.set(.single(self.listStateValue)) if case .admins(nil) = self.category, case .ready = self.listStateValue.loadingState { let ids: Set = Set(self.listStateValue.list.map { $0.peer.id }) let previousIds: Set = Set(oldValue.list.map { $0.peer.id }) if ids != previousIds { let _ = updateCachedChannelAdminIds(postbox: self.postbox, peerId: self.peerId, ids: ids).start() } } } } private var listStatePromise: Promise var listState: Signal { return self.listStatePromise.get() } private let loadingDisposable = MetaDisposable() private let headUpdateDisposable = MetaDisposable() private var headUpdateTimer: SwiftSignalKit.Timer? init(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, category: ChannelMemberListCategory) { self.postbox = postbox self.network = network self.accountPeerId = accountPeerId self.peerId = peerId self.category = category self.listStateValue = ChannelMemberListState(list: [], loadingState: .ready(hasMore: true)) self.listStatePromise = Promise(self.listStateValue) self.loadMoreInternal(initial: true) } deinit { self.loadingDisposable.dispose() self.headUpdateDisposable.dispose() self.headUpdateTimer?.invalidate() } func loadMore() { self.loadMoreInternal(initial: false) } private func loadMoreInternal(initial: Bool) { guard case .ready(true) = self.listStateValue.loadingState else { return } let loadCount: Int32 if case .ready(true) = self.listStateValue.loadingState, self.listStateValue.list.isEmpty { loadCount = initialBatchSize } else { loadCount = requestBatchSize } self.listStateValue = self.listStateValue.withUpdatedLoadingState(.loading(initial: initial)) self.loadingDisposable.set((self.loadMoreSignal(count: loadCount) |> deliverOnMainQueue).start(next: { [weak self] members in self?.appendMembersAndFinishLoading(members) })) } func reset(_ force: Bool) { if case .loading = self.listStateValue.loadingState, self.listStateValue.list.isEmpty { } else { var list = self.listStateValue.list var loadingState: ChannelMemberListLoadingState = .ready(hasMore: true) if list.count > Int(initialBatchSize) && !force { list.removeSubrange(Int(initialBatchSize) ..< list.count) loadingState = .ready(hasMore: true) } self.loadingDisposable.set(nil) self.listStateValue = self.listStateValue.withUpdatedLoadingState(loadingState).withUpdatedList(list) } } private func loadSignal(offset: Int32, count: Int32, hash: Int32) -> Signal<[RenderedChannelParticipant]?, NoError> { let requestCategory: ChannelMembersCategory var adminQuery: String? = nil switch self.category { case .recent: requestCategory = .recent(.all) case let .recentSearch(query): requestCategory = .recent(.search(query)) case let .admins(query): requestCategory = .admins adminQuery = query case let .restricted(query): requestCategory = .restricted(query != nil ? .search(query!) : .all) case let .banned(query): requestCategory = .banned(query != nil ? .search(query!) : .all) } return channelMembers(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, category: requestCategory, offset: offset, limit: count, hash: hash) |> map { members in switch requestCategory { case .admins: if let query = adminQuery { return members?.filter({$0.peer.displayTitle.lowercased().components(separatedBy: " ").contains(where: {$0.hasPrefix(query.lowercased())})}) } default: break } return members } } private func loadMoreSignal(count: Int32) -> Signal<[RenderedChannelParticipant], NoError> { return self.loadSignal(offset: Int32(self.listStateValue.list.count), count: count, hash: 0) |> map { value -> [RenderedChannelParticipant] in return value ?? [] } } private func updateHeadMembers(_ headMembers: [RenderedChannelParticipant]?) { if let headMembers = headMembers { var existingIds = Set() var list = headMembers for member in list { existingIds.insert(member.peer.id) } for member in self.listStateValue.list { if !existingIds.contains(member.peer.id) { list.append(member) } } self.loadingDisposable.set(nil) self.listStateValue = self.listStateValue.withUpdatedList(list) if case .loading = self.listStateValue.loadingState { self.loadMore() } } self.headUpdateTimer?.invalidate() self.headUpdateTimer = nil self.checkUpdateHead() } private func appendMembersAndFinishLoading(_ members: [RenderedChannelParticipant]) { var firstLoad = false if case .loading = self.listStateValue.loadingState, self.listStateValue.list.isEmpty { firstLoad = true } var existingIds = Set() var list = self.listStateValue.list for member in list { existingIds.insert(member.peer.id) } for member in members { if !existingIds.contains(member.peer.id) { list.append(member) } } self.listStateValue = self.listStateValue.withUpdatedList(list).withUpdatedLoadingState(.ready(hasMore: members.count >= requestBatchSize)) if firstLoad { self.checkUpdateHead() } } func forceUpdateHead() { self.headUpdateTimer = nil self.checkUpdateHead() } private func checkUpdateHead() { if self.listStateValue.list.isEmpty { return } if self.headUpdateTimer == nil { let headUpdateTimer = SwiftSignalKit.Timer(timeout: headUpdateTimeout, repeat: false, completion: { [weak self] in guard let strongSelf = self else { return } var hash: UInt32 = 0 for i in 0 ..< min(strongSelf.listStateValue.list.count, Int(initialBatchSize)) { let peerId = strongSelf.listStateValue.list[i].peer.id hash = (hash &* 20261) &+ UInt32(peerId.id) } hash = hash % 0x7FFFFFFF strongSelf.headUpdateDisposable.set((strongSelf.loadSignal(offset: 0, count: initialBatchSize, hash: Int32(bitPattern: hash)) |> deliverOnMainQueue).start(next: { members in self?.updateHeadMembers(members) })) }, queue: Queue.mainQueue()) self.headUpdateTimer = headUpdateTimer headUpdateTimer.start() } } fileprivate func replayUpdates(_ updates: [(ChannelParticipant?, RenderedChannelParticipant?, Bool?)]) { var list = self.listStateValue.list var updatedList = false for (maybePrevious, updated, infoIsMember) in updates { var previous: ChannelParticipant? = maybePrevious if let participantId = maybePrevious?.peerId ?? updated?.peer.id { inner: for participant in list { if participant.peer.id == participantId { previous = participant.participant break inner } } } switch self.category { case let .admins(query): if let updated = updated, let _ = updated.participant.adminInfo, (query == nil || updated.peer.indexName.matchesByTokens(query!)) { var found = false loop: for i in 0 ..< list.count { if list[i].peer.id == updated.peer.id { list[i] = updated found = true updatedList = true break loop } } if !found { list.insert(updated, at: 0) updatedList = true } } else if let previous = previous, let _ = previous.adminInfo { loop: for i in 0 ..< list.count { if list[i].peer.id == previous.peerId { list.remove(at: i) updatedList = true break loop } } } case .restricted: if let updated = updated, let banInfo = updated.participant.banInfo, !banInfo.rights.flags.contains(.banReadMessages) { var found = false loop: for i in 0 ..< list.count { if list[i].peer.id == updated.peer.id { list[i] = updated found = true updatedList = true break loop } } if !found { list.insert(updated, at: 0) updatedList = true } } else if let previous = previous, let banInfo = previous.banInfo, !banInfo.rights.flags.contains(.banReadMessages) { loop: for i in 0 ..< list.count { if list[i].peer.id == previous.peerId { list.remove(at: i) updatedList = true break loop } } } case .banned: if let updated = updated, let banInfo = updated.participant.banInfo, banInfo.rights.flags.contains(.banReadMessages) { var found = false loop: for i in 0 ..< list.count { if list[i].peer.id == updated.peer.id { list[i] = updated found = true updatedList = true break loop } } if !found { list.insert(updated, at: 0) updatedList = true } } else if let previous = previous, let banInfo = previous.banInfo, banInfo.rights.flags.contains(.banReadMessages) { loop: for i in 0 ..< list.count { if list[i].peer.id == previous.peerId { list.remove(at: i) updatedList = true break loop } } } case .recent: if let updated = updated, isParticipantMember(updated.participant, infoIsMember: infoIsMember) { var found = false loop: for i in 0 ..< list.count { if list[i].peer.id == updated.peer.id { list[i] = updated found = true updatedList = true break loop } } if !found { list.insert(updated, at: 0) updatedList = true } } else if let previous = previous, isParticipantMember(previous, infoIsMember: nil) { loop: for i in 0 ..< list.count { if list[i].peer.id == previous.peerId { list.remove(at: i) updatedList = true break loop } } } case let .recentSearch(query): if let updated = updated, isParticipantMember(updated.participant, infoIsMember: infoIsMember), updated.peer.indexName.matchesByTokens(query) { var found = false loop: for i in 0 ..< list.count { if list[i].peer.id == updated.peer.id { list[i] = updated found = true updatedList = true break loop } } if !found { list.insert(updated, at: 0) updatedList = true } } else if let previous = previous, isParticipantMember(previous, infoIsMember: nil) { loop: for i in 0 ..< list.count { if list[i].peer.id == previous.peerId { list.remove(at: i) updatedList = true break loop } } } } } if updatedList { self.listStateValue = self.listStateValue.withUpdatedList(list) } } } private final class ChannelMemberMultiCategoryListContext: ChannelMemberCategoryListContext { private var contexts: [ChannelMemberSingleCategoryListContext] = [] var listStateValue: ChannelMemberListState { return ChannelMemberMultiCategoryListContext.reduceListStates(self.contexts.map { $0.listStateValue }) } private static func reduceListStates(_ listStates: [ChannelMemberListState]) -> ChannelMemberListState { var allReady = true for listState in listStates { if case .loading(true) = listState.loadingState, listState.list.isEmpty { allReady = false break } } if !allReady { return ChannelMemberListState(list: [], loadingState: .loading(initial: true)) } var list: [RenderedChannelParticipant] = [] var existingIds = Set() var loadingState: ChannelMemberListLoadingState = .ready(hasMore: false) loop: for i in 0 ..< listStates.count { for item in listStates[i].list { if !existingIds.contains(item.peer.id) { existingIds.insert(item.peer.id) list.append(item) } } switch listStates[i].loadingState { case let .loading(initial): loadingState = .loading(initial: initial) break loop case let .ready(hasMore): if hasMore { loadingState = .ready(hasMore: true) break loop } } } return ChannelMemberListState(list: list, loadingState: loadingState) } var listState: Signal { let signals: [Signal] = self.contexts.map { context in return context.listState } return combineLatest(signals) |> map { listStates -> ChannelMemberListState in return ChannelMemberMultiCategoryListContext.reduceListStates(listStates) } } init(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, categories: [ChannelMemberListCategory]) { self.contexts = categories.map { category in return ChannelMemberSingleCategoryListContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, category: category) } } func loadMore() { loop: for context in self.contexts { switch context.listStateValue.loadingState { case .loading: break loop case let .ready(hasMore): if hasMore { context.loadMore() } } } } func reset(_ force: Bool) { for context in self.contexts { context.reset(force) } } func forceUpdateHead() { for context in self.contexts { context.forceUpdateHead() } } func replayUpdates(_ updates: [(ChannelParticipant?, RenderedChannelParticipant?, Bool?)]) { for context in self.contexts { context.replayUpdates(updates) } } } struct PeerChannelMemberCategoryControl { fileprivate let key: PeerChannelMemberContextKey } private final class PeerChannelMemberContextWithSubscribers { let context: ChannelMemberCategoryListContext private let emptyTimeout: Double private let subscribers = Bag<(ChannelMemberListState) -> Void>() private let disposable = MetaDisposable() private let becameEmpty: () -> Void private var emptyTimer: SwiftSignalKit.Timer? init(context: ChannelMemberCategoryListContext, emptyTimeout: Double, becameEmpty: @escaping () -> Void) { self.context = context self.emptyTimeout = emptyTimeout self.becameEmpty = becameEmpty self.disposable.set((context.listState |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self { for f in strongSelf.subscribers.copyItems() { f(value) } } })) } deinit { self.disposable.dispose() self.emptyTimer?.invalidate() } private func resetAndBeginEmptyTimer() { self.context.reset(false) self.emptyTimer?.invalidate() let emptyTimer = SwiftSignalKit.Timer(timeout: self.emptyTimeout, repeat: false, completion: { [weak self] in if let strongSelf = self { if strongSelf.subscribers.isEmpty { strongSelf.becameEmpty() } } }, queue: Queue.mainQueue()) self.emptyTimer = emptyTimer emptyTimer.start() } func subscribe(requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> Disposable { let wasEmpty = self.subscribers.isEmpty let index = self.subscribers.add(updated) updated(self.context.listStateValue) if wasEmpty { self.emptyTimer?.invalidate() if requestUpdate { self.context.forceUpdateHead() } } return ActionDisposable { [weak self] in Queue.mainQueue().async { if let strongSelf = self { strongSelf.subscribers.remove(index) if strongSelf.subscribers.isEmpty { strongSelf.resetAndBeginEmptyTimer() } } } } } } final class PeerChannelMemberCategoriesContext { private let postbox: Postbox private let network: Network private let accountPeerId: PeerId private let peerId: PeerId private var becameEmpty: (Bool) -> Void private var contexts: [PeerChannelMemberContextKey: PeerChannelMemberContextWithSubscribers] = [:] init(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, becameEmpty: @escaping (Bool) -> Void) { self.postbox = postbox self.network = network self.accountPeerId = accountPeerId self.peerId = peerId self.becameEmpty = becameEmpty } func reset(_ key: PeerChannelMemberContextKey) { for (contextKey, context) in contexts { if contextKey == key { context.context.reset(true) context.context.loadMore() } } } func getContext(key: PeerChannelMemberContextKey, requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl) { assert(Queue.mainQueue().isCurrent()) if let current = self.contexts[key] { return (current.subscribe(requestUpdate: requestUpdate, updated: updated), PeerChannelMemberCategoryControl(key: key)) } let context: ChannelMemberCategoryListContext let emptyTimeout: Double switch key { case .admins(nil), .banned(nil), .recentSearch(nil), .restricted(nil), .restrictedAndBanned(nil): emptyTimeout = defaultEmptyTimeout default: emptyTimeout = 0.0 } switch key { case .recent, .recentSearch, .admins: let mappedCategory: ChannelMemberListCategory switch key { case .recent: mappedCategory = .recent case let .recentSearch(query): mappedCategory = .recentSearch(query) case let .admins(query): mappedCategory = .admins(query) default: mappedCategory = .recent } context = ChannelMemberSingleCategoryListContext(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, category: mappedCategory) case let .restrictedAndBanned(query): context = ChannelMemberMultiCategoryListContext(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, categories: [.restricted(query), .banned(query)]) case let .restricted(query): context = ChannelMemberSingleCategoryListContext(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, category: .restricted(query)) case let .banned(query): context = ChannelMemberSingleCategoryListContext(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, category: .banned(query)) } let contextWithSubscribers = PeerChannelMemberContextWithSubscribers(context: context, emptyTimeout: emptyTimeout, becameEmpty: { [weak self] in assert(Queue.mainQueue().isCurrent()) if let strongSelf = self { strongSelf.contexts.removeValue(forKey: key) } }) self.contexts[key] = contextWithSubscribers return (contextWithSubscribers.subscribe(requestUpdate: requestUpdate, updated: updated), PeerChannelMemberCategoryControl(key: key)) } func loadMore(_ control: PeerChannelMemberCategoryControl) { assert(Queue.mainQueue().isCurrent()) if let context = self.contexts[control.key] { context.context.loadMore() } } func replayUpdates(_ updates: [(ChannelParticipant?, RenderedChannelParticipant?, Bool?)]) { for (_, context) in self.contexts { context.context.replayUpdates(updates) } } }