import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import Postbox import TelegramCore enum ChannelMembersSearchMode { case searchMembers case searchAdmins case searchBanned case searchKicked case banAndPromoteActions case inviteActions } private enum ChannelMembersSearchSection { case none case members case banned case contacts case bots case admins case global var chatListHeaderType: ChatListSearchItemHeaderType? { switch self { case .none: return nil case .members: return .members case .banned: return .exceptions case .contacts: return .contacts case .bots: return .bots case .admins: return .admins case .global: return .globalPeers } } } private enum ChannelMembersSearchContent: Equatable { case peer(Peer) case participant(participant: RenderedChannelParticipant, label: String?, revealActions: [ParticipantRevealAction], revealed: Bool, enabled: Bool) static func ==(lhs: ChannelMembersSearchContent, rhs: ChannelMembersSearchContent) -> Bool { switch lhs { case let .peer(lhsPeer): if case let .peer(rhsPeer) = rhs { return lhsPeer.isEqual(rhsPeer) } else { return false } case let .participant(participant, label, revealActions, revealed, enabled): if case .participant(participant, label, revealActions, revealed, enabled) = rhs { return true } else { return false } } } var peerId: PeerId { switch self { case let .peer(peer): return peer.id case let .participant(participant, _, _, _, _): return participant.peer.id } } } private struct RevealedPeerId: Equatable { let peerId: PeerId let section: ChannelMembersSearchSection } private final class ChannelMembersSearchContainerInteraction { let peerSelected: (Peer, RenderedChannelParticipant?) -> Void let setPeerIdWithRevealedOptions: (RevealedPeerId?, RevealedPeerId?) -> Void let promotePeer: (RenderedChannelParticipant) -> Void let restrictPeer: (RenderedChannelParticipant) -> Void let removePeer: (PeerId) -> Void init(peerSelected: @escaping (Peer, RenderedChannelParticipant?) -> Void, setPeerIdWithRevealedOptions: @escaping (RevealedPeerId?, RevealedPeerId?) -> Void, promotePeer: @escaping (RenderedChannelParticipant) -> Void, restrictPeer: @escaping (RenderedChannelParticipant) -> Void, removePeer: @escaping (PeerId) -> Void) { self.peerSelected = peerSelected self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.promotePeer = promotePeer self.restrictPeer = restrictPeer self.removePeer = removePeer } } private struct ChannelMembersSearchEntryId: Hashable { let peerId: PeerId let section: ChannelMembersSearchSection } private final class ChannelMembersSearchEntry: Comparable, Identifiable { let index: Int let content: ChannelMembersSearchContent let section: ChannelMembersSearchSection let dateTimeFormat: PresentationDateTimeFormat let addIcon: Bool init(index: Int, content: ChannelMembersSearchContent, section: ChannelMembersSearchSection, dateTimeFormat: PresentationDateTimeFormat, addIcon: Bool = false) { self.index = index self.content = content self.section = section self.dateTimeFormat = dateTimeFormat self.addIcon = addIcon } var stableId: ChannelMembersSearchEntryId { return ChannelMembersSearchEntryId(peerId: self.content.peerId, section: self.section) } static func ==(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool { return lhs.index == rhs.index && lhs.content == rhs.content && lhs.section == rhs.section && lhs.addIcon == rhs.addIcon } static func <(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool { return lhs.index < rhs.index } func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchContainerInteraction) -> ListViewItem { switch self.content { case let .peer(peer): return ContactsPeerItem(theme: theme, strings: strings, sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, account: account, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in interaction.peerSelected(peer, nil) }) case let .participant(participant, label, revealActions, revealed, enabled): let status: ContactsPeerItemStatus if let label = label { status = .custom(label) } else if let presence = participant.presences[participant.peer.id], self.addIcon { status = .presence(presence, dateTimeFormat) } else { status = .none } var options: [ItemListPeerItemRevealOption] = [] for action in revealActions { options.append(ItemListPeerItemRevealOption(type: action.type, title: action.title, action: { switch action.action { case .promote: interaction.promotePeer(participant) break case .restrict: interaction.restrictPeer(participant) break case .remove: interaction.removePeer(participant.peer.id) break } })) } var actionIcon: ContactsPeerItemActionIcon = .none if self.addIcon { actionIcon = .add } return ContactsPeerItem(theme: theme, strings: strings, sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, account: account, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: participant.peer), status: status, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: revealed), options: options, actionIcon: actionIcon, index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in interaction.peerSelected(participant.peer, participant) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in interaction.setPeerIdWithRevealedOptions(RevealedPeerId(peerId: participant.peer.id, section: self.section), fromPeerId.flatMap({ RevealedPeerId(peerId: $0, section: self.section) })) }) } } } struct ChannelMembersSearchContainerTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] let isSearching: Bool } private enum GroupMemberCategory { case contacts case admins case bots case members } private func categorySignal(context: AccountContext, peerId: PeerId, category: GroupMemberCategory) -> Signal<[RenderedChannelParticipant], NoError> { return Signal<[RenderedChannelParticipant], NoError> { subscriber in let disposableAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?) func processListState(_ listState: ChannelMemberListState) { assert(Queue.mainQueue().isCurrent()) var process = false if case .ready = listState.loadingState { process = true } else if !listState.list.isEmpty { process = true } if process { subscriber.putNext(listState.list) } } switch category { case .admins: disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.admins(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: processListState) case .contacts: disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.contacts(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: processListState) case .bots: disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.bots(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: processListState) case .members: disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: processListState) } let (disposable, _) = disposableAndLoadMoreControl return disposable } |> runOn(.mainQueue()) } private struct GroupMembersSearchContextState { var contacts: [RenderedChannelParticipant] = [] var admins: [RenderedChannelParticipant] = [] var bots: [RenderedChannelParticipant] = [] var members: [RenderedChannelParticipant] = [] } final class GroupMembersSearchContext { fileprivate let state = Promise() init(context: AccountContext, peerId: PeerId) { assert(Queue.mainQueue().isCurrent()) let combinedSignal = combineLatest(queue: .mainQueue(), categorySignal(context: context, peerId: peerId, category: .contacts), categorySignal(context: context, peerId: peerId, category: .bots), categorySignal(context: context, peerId: peerId, category: .admins), categorySignal(context: context, peerId: peerId, category: .members)) |> map { contacts, bots, admins, members -> GroupMembersSearchContextState in let contactPeerIds = Set(contacts.map({ $0.peer.id })) let adminPeerIds = Set(admins.map({ $0.peer.id })) let botPeerIds = Set(bots.map({ $0.peer.id })) var excludeMemberPeerIds = contactPeerIds excludeMemberPeerIds.formUnion(adminPeerIds) excludeMemberPeerIds.formUnion(botPeerIds) let filteredMembers = members.filter({ !excludeMemberPeerIds.contains($0.peer.id) }) return GroupMembersSearchContextState(contacts: contacts, admins: admins, bots: bots, members: filteredMembers) } self.state.set(combinedSignal) } } private func channelMembersSearchContainerPreparedRecentTransition(from fromEntries: [ChannelMembersSearchEntry], to toEntries: [ChannelMembersSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchContainerInteraction) -> ChannelMembersSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } return ChannelMembersSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) } private struct ChannelMembersSearchContainerState: Equatable { var revealedPeerId: RevealedPeerId? var removingParticipantIds = Set() } final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNode { private let context: AccountContext private let openPeer: (Peer, RenderedChannelParticipant?) -> Void private let mode: ChannelMembersSearchMode private let emptyQueryListNode: ListView private let listNode: ListView private var enqueuedEmptyQueryTransitions: [(ChannelMembersSearchContainerTransition, Bool)] = [] private var enqueuedTransitions: [(ChannelMembersSearchContainerTransition, Bool)] = [] private var hasValidLayout = false private let searchQuery = Promise() private let emptyQueryDisposable = MetaDisposable() private let searchDisposable = MetaDisposable() private var presentationData: PresentationData private var presentationDataDisposable: Disposable? private let removeMemberDisposable = MetaDisposable() private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PresentationDateTimeFormat)> init(context: AccountContext, peerId: PeerId, mode: ChannelMembersSearchMode, filters: [ChannelMembersSearchFilter], searchContext: GroupMembersSearchContext?, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void, updateActivity: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.openPeer = openPeer self.mode = mode self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.nameSortOrder, self.presentationData.nameDisplayOrder, self.presentationData.dateTimeFormat)) self.emptyQueryListNode = ListView() self.listNode = ListView() super.init() self.emptyQueryListNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.listNode.isHidden = true self.addSubnode(self.emptyQueryListNode) self.addSubnode(self.listNode) let statePromise = ValuePromise(ChannelMembersSearchContainerState(), ignoreRepeated: true) let stateValue = Atomic(value: ChannelMembersSearchContainerState()) let updateState: ((ChannelMembersSearchContainerState) -> ChannelMembersSearchContainerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } let removeMemberDisposable = self.removeMemberDisposable let interaction = ChannelMembersSearchContainerInteraction(peerSelected: { peer, participant in openPeer(peer, participant) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in var state = state if (peerId == nil && fromPeerId == state.revealedPeerId) || (peerId != nil && fromPeerId == nil) { state.revealedPeerId = peerId } return state } }, promotePeer: { participant in updateState { state in var state = state state.revealedPeerId = nil return state } present(channelAdminController(context: context, peerId: peerId, adminId: participant.peer.id, initialParticipant: participant.participant, updated: { _ in }, upgradedToSupergroup: { _, f in f() }, transferedOwnership: { _ in }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, restrictPeer: { participant in updateState { state in var state = state state.revealedPeerId = nil return state } present(channelBannedMemberController(context: context, peerId: peerId, memberId: participant.peer.id, initialParticipant: participant.participant, updated: { _ in }, upgradedToSupergroup: { _, f in f() }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, removePeer: { memberId in updateState { state in var state = state state.revealedPeerId = nil return state } let signal = context.account.postbox.loadedPeerWithId(memberId) |> deliverOnMainQueue |> mapToSignal { peer -> Signal in let result = ValuePromise() result.set(true) return result.get() } |> mapToSignal { value -> Signal in if value { updateState { state in var state = state state.removingParticipantIds.insert(memberId) return state } if peerId.namespace == Namespaces.Peer.CloudChannel { if case .searchAdmins = mode { return context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: context.account, peerId: peerId, memberId: memberId, adminRights: TelegramChatAdminRights(flags: [])) |> `catch` { _ -> Signal in return .complete() } |> afterDisposed { Queue.mainQueue().async { updateState { state in var state = state state.removingParticipantIds.remove(memberId) return state } } } } return context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: context.account, peerId: peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) |> afterDisposed { Queue.mainQueue().async { updateState { state in var state = state state.removingParticipantIds.remove(memberId) return state } } } } if case .searchAdmins = mode { return removeGroupAdmin(account: context.account, peerId: peerId, adminId: memberId) |> `catch` { _ -> Signal in return .complete() } |> deliverOnMainQueue |> afterDisposed { updateState { state in var state = state state.removingParticipantIds.remove(memberId) return state } } } return removePeerMember(account: context.account, peerId: peerId, memberId: memberId) |> deliverOnMainQueue |> afterDisposed { updateState { state in var state = state state.removingParticipantIds.remove(memberId) return state } } } else { return .complete() } } removeMemberDisposable.set(signal.start()) }) let themeAndStringsPromise = self.themeAndStringsPromise let emptyQueryItems: Signal<[ChannelMembersSearchEntry]?, NoError> if let searchContext = searchContext { emptyQueryItems = combineLatest(queue: .mainQueue(), statePromise.get(), searchContext.state.get(), context.account.postbox.peerView(id: peerId) |> take(1), themeAndStringsPromise.get()) |> map { state, searchState, peerView, themeAndStrings -> [ChannelMembersSearchEntry]? in if let channel = peerView.peers[peerId] as? TelegramChannel { var entries: [ChannelMembersSearchEntry] = [] var index = 0 func processParticipant(participant: RenderedChannelParticipant, section: ChannelMembersSearchSection) { var canPromote: Bool var canRestrict: Bool switch participant.participant { case .creator: canPromote = false canRestrict = false case let .member(_, _, adminRights, bannedRights): if channel.hasPermission(.addAdmins) { canPromote = true } else { canPromote = false } if channel.hasPermission(.banMembers) { canRestrict = true } else { canRestrict = false } if canPromote { if let bannedRights = bannedRights { if bannedRights.restrictedBy != context.account.peerId && !channel.flags.contains(.isCreator) { canPromote = false } } } if canRestrict { if let adminRights = adminRights { if adminRights.promotedBy != context.account.peerId && !channel.flags.contains(.isCreator) { canRestrict = false } } } } var label: String? var enabled = true if case .searchMembers = mode { switch participant.participant { case .creator: label = themeAndStrings.1.Channel_Management_LabelOwner default: break } } if state.removingParticipantIds.contains(participant.peer.id) { enabled = false } var peerActions: [ParticipantRevealAction] = [] if case .searchMembers = mode { if canPromote { peerActions.append(ParticipantRevealAction(type: .neutral, title: themeAndStrings.1.GroupInfo_ActionPromote, action: .promote)) } if canRestrict { peerActions.append(ParticipantRevealAction(type: .warning, title: themeAndStrings.1.GroupInfo_ActionRestrict, action: .restrict)) peerActions.append(ParticipantRevealAction(type: .destructive, title: themeAndStrings.1.Common_Delete, action: .remove)) } } entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: peerActions, revealed: state.revealedPeerId == RevealedPeerId(peerId: participant.peer.id, section: section), enabled: enabled), section: section, dateTimeFormat: themeAndStrings.4)) index += 1 } for participant in searchState.contacts { processParticipant(participant: participant, section: .contacts) } for participant in searchState.bots { processParticipant(participant: participant, section: .bots) } for participant in searchState.admins { processParticipant(participant: participant, section: .admins) } for participant in searchState.members { processParticipant(participant: participant, section: .members) } return entries } else { return nil } } } else { emptyQueryItems = .single(nil) } let foundItems = combineLatest(searchQuery.get(), context.account.postbox.peerView(id: peerId) |> take(1)) |> mapToSignal { query, peerView -> Signal<[ChannelMembersSearchEntry]?, NoError> in guard let query = query, !query.isEmpty else { return .single(nil) } if let channel = peerView.peers[peerId] as? TelegramChannel { updateActivity(true) let foundGroupMembers: Signal<[RenderedChannelParticipant], NoError> let foundMembers: Signal<[RenderedChannelParticipant], NoError> switch mode { case .searchMembers, .banAndPromoteActions: foundGroupMembers = Signal { subscriber in let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in if case .ready = state.loadingState { subscriber.putNext(state.list) } }) return disposable } |> runOn(Queue.mainQueue()) foundMembers = .single([]) case .inviteActions: foundGroupMembers = .single([]) foundMembers = channelMembers(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, category: .recent(.search(query))) |> map { $0 ?? [] } case .searchAdmins: foundGroupMembers = Signal { subscriber in let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.admins(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in if case .ready = state.loadingState { subscriber.putNext(state.list) } }) return disposable } |> runOn(Queue.mainQueue()) foundMembers = .single([]) case .searchBanned: foundGroupMembers = Signal { subscriber in let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.restricted(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in if case .ready = state.loadingState { subscriber.putNext(state.list) subscriber.putCompletion() } }) return disposable } |> runOn(Queue.mainQueue()) foundMembers = Signal { subscriber in let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in if case .ready = state.loadingState { subscriber.putNext(state.list.filter({ participant in return participant.peer.id != context.account.peerId })) } }) return disposable } |> runOn(Queue.mainQueue()) case .searchKicked: foundGroupMembers = Signal { subscriber in let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.banned(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in if case .ready = state.loadingState { subscriber.putNext(state.list) subscriber.putCompletion() } }) return disposable } |> runOn(Queue.mainQueue()) foundMembers = .single([]) } let foundContacts: Signal<([Peer], [PeerId: PeerPresence]), NoError> let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> switch mode { case .inviteActions, .banAndPromoteActions: foundContacts = context.account.postbox.searchContacts(query: query.lowercased()) foundRemotePeers = .single(([], [])) |> then(searchPeers(account: context.account, query: query) |> delay(0.2, queue: Queue.concurrentDefaultQueue())) case .searchMembers, .searchBanned, .searchKicked, .searchAdmins: foundContacts = .single(([], [:])) foundRemotePeers = .single(([], [])) } return combineLatest(foundGroupMembers, foundMembers, foundContacts, foundRemotePeers, themeAndStringsPromise.get(), statePromise.get()) |> map { foundGroupMembers, foundMembers, foundContacts, foundRemotePeers, themeAndStrings, state -> [ChannelMembersSearchEntry]? in var entries: [ChannelMembersSearchEntry] = [] var existingPeerIds = Set() for filter in filters { switch filter { case let .exclude(ids): existingPeerIds = existingPeerIds.union(ids) case .disable: break } } switch mode { case .inviteActions, .banAndPromoteActions: existingPeerIds.insert(context.account.peerId) case .searchMembers, .searchAdmins, .searchBanned, .searchKicked: break } var index = 0 for participant in foundGroupMembers { if !existingPeerIds.contains(participant.peer.id) { existingPeerIds.insert(participant.peer.id) let section: ChannelMembersSearchSection switch mode { case .inviteActions, .banAndPromoteActions: section = .members case .searchBanned: section = .banned case .searchMembers, .searchKicked, .searchAdmins: section = .none } var canPromote: Bool var canRestrict: Bool switch participant.participant { case .creator: canPromote = false canRestrict = false case let .member(_, _, adminRights, bannedRights): if channel.hasPermission(.addAdmins) { canPromote = true } else { canPromote = false } if channel.hasPermission(.banMembers) { canRestrict = true } else { canRestrict = false } if canPromote { if let bannedRights = bannedRights { if bannedRights.restrictedBy != context.account.peerId && !channel.flags.contains(.isCreator) { canPromote = false } } } if canRestrict { if let adminRights = adminRights { if adminRights.promotedBy != context.account.peerId && !channel.flags.contains(.isCreator) { canRestrict = false } } } } var label: String? var enabled = true if case .banAndPromoteActions = mode { if case .creator = participant.participant { label = themeAndStrings.1.Channel_Management_LabelOwner enabled = false } } else if case .searchMembers = mode { switch participant.participant { case .creator: label = themeAndStrings.1.Channel_Management_LabelOwner case let .member(member): if member.adminInfo != nil { label = themeAndStrings.1.Channel_Management_LabelEditor } } } if state.removingParticipantIds.contains(participant.peer.id) { enabled = false } var peerActions: [ParticipantRevealAction] = [] if case .searchMembers = mode { if canPromote { peerActions.append(ParticipantRevealAction(type: .neutral, title: themeAndStrings.1.GroupInfo_ActionPromote, action: .promote)) } if canRestrict { peerActions.append(ParticipantRevealAction(type: .warning, title: themeAndStrings.1.GroupInfo_ActionRestrict, action: .restrict)) peerActions.append(ParticipantRevealAction(type: .destructive, title: themeAndStrings.1.Common_Delete, action: .remove)) } } else if case .searchAdmins = mode { if canRestrict { peerActions.append(ParticipantRevealAction(type: .destructive, title: themeAndStrings.1.Common_Delete, action: .remove)) } } switch mode { case .searchAdmins: switch participant.participant { case .creator: label = themeAndStrings.1.Channel_Management_LabelOwner case let .member(_, _, adminInfo, _): if let adminInfo = adminInfo { if let peer = participant.peers[adminInfo.promotedBy] { if peer.id == participant.peer.id { label = themeAndStrings.1.Channel_Management_LabelAdministrator } else { label = themeAndStrings.1.Channel_Management_PromotedBy(peer.displayTitle).0 } } } } case .searchBanned: switch participant.participant { case let .member(_, _, _, banInfo): if let banInfo = banInfo { var exceptionsString = "" for rights in allGroupPermissionList { if banInfo.rights.flags.contains(rights) { if !exceptionsString.isEmpty { exceptionsString.append(", ") } exceptionsString.append(compactStringForGroupPermission(strings: themeAndStrings.1, right: rights)) } } label = exceptionsString } default: break } case .searchKicked: switch participant.participant { case let .member(_, _, _, banInfo): if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { label = themeAndStrings.1.Channel_Management_RemovedBy(peer.displayTitle).0 } default: break } default: break } entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: peerActions, revealed: state.revealedPeerId == RevealedPeerId(peerId: participant.peer.id, section: section), enabled: enabled), section: section, dateTimeFormat: themeAndStrings.4)) index += 1 } } for participant in foundMembers { if !existingPeerIds.contains(participant.peer.id) { existingPeerIds.insert(participant.peer.id) let section: ChannelMembersSearchSection var addIcon = false switch mode { case .inviteActions, .banAndPromoteActions: section = .members case .searchBanned: section = .members addIcon = true case .searchMembers, .searchKicked, .searchAdmins: section = .none } var label: String? var enabled = true if case .banAndPromoteActions = mode { if case .creator = participant.participant { label = themeAndStrings.1.Channel_Management_LabelOwner enabled = false } } entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: [], revealed: false, enabled: enabled), section: section, dateTimeFormat: themeAndStrings.4, addIcon: addIcon)) index += 1 } } for peer in foundContacts.0 { if !existingPeerIds.contains(peer.id) { existingPeerIds.insert(peer.id) entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .contacts, dateTimeFormat: themeAndStrings.4)) index += 1 } } for foundPeer in foundRemotePeers.0 { let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && peer is TelegramUser { existingPeerIds.insert(peer.id) entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: themeAndStrings.4)) index += 1 } } for foundPeer in foundRemotePeers.1 { let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && peer is TelegramUser { existingPeerIds.insert(peer.id) entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: themeAndStrings.4)) index += 1 } } return entries } } else if let group = peerView.peers[peerId] as? TelegramGroup, let cachedData = peerView.cachedData as? CachedGroupData { updateActivity(true) let foundGroupMembers: Signal<[RenderedChannelParticipant], NoError> let foundMembers: Signal<[RenderedChannelParticipant], NoError> switch mode { case .searchMembers, .banAndPromoteActions: var matchingMembers: [RenderedChannelParticipant] = [] if let participants = cachedData.participants { for participant in participants.participants { guard let peer = peerView.peers[participant.peerId] else { continue } if !peer.indexName.matchesByTokens(query.lowercased()) { continue } var creatorPeer: Peer? for participant in participants.participants { if let peer = peerView.peers[participant.peerId] { switch participant { case .creator: creatorPeer = peer default: break } } } let renderedParticipant: RenderedChannelParticipant switch participant { case .creator: renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id), peer: peer) case .admin: var peers: [PeerId: Peer] = [:] if let creator = creatorPeer { peers[creator.id] = creator } peers[peer.id] = peer renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(flags: .groupSpecific), promotedBy: creatorPeer?.id ?? context.account.peerId, canBeEditedByAccountPeer: creatorPeer?.id == context.account.peerId), banInfo: nil), peer: peer, peers: peers) case .member: var peers: [PeerId: Peer] = [:] peers[peer.id] = peer renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil), peer: peer, peers: peers) } matchingMembers.append(renderedParticipant) } } foundGroupMembers = .single(matchingMembers) foundMembers = .single([]) case .inviteActions: foundGroupMembers = .single([]) foundMembers = .single([]) case .searchAdmins: foundGroupMembers = .single([]) foundMembers = .single([]) case .searchBanned: foundGroupMembers = .single([]) foundMembers = .single([]) case .searchKicked: foundGroupMembers = .single([]) foundMembers = .single([]) } return combineLatest(foundGroupMembers, foundMembers, themeAndStringsPromise.get(), statePromise.get()) |> map { foundGroupMembers, foundMembers, themeAndStrings, state -> [ChannelMembersSearchEntry]? in var entries: [ChannelMembersSearchEntry] = [] var existingPeerIds = Set() for filter in filters { switch filter { case let .exclude(ids): existingPeerIds = existingPeerIds.union(ids) case .disable: break } } switch mode { case .inviteActions, .banAndPromoteActions: existingPeerIds.insert(context.account.peerId) case .searchMembers, .searchAdmins, .searchBanned, .searchKicked: break } var index = 0 for participant in foundGroupMembers { if !existingPeerIds.contains(participant.peer.id) { existingPeerIds.insert(participant.peer.id) let section: ChannelMembersSearchSection switch mode { case .inviteActions, .banAndPromoteActions: section = .members case .searchBanned: section = .banned case .searchMembers, .searchKicked, .searchAdmins: section = .none } var canPromote: Bool = false var canRestrict: Bool = false /*switch participant.participant { case .creator: canPromote = false canRestrict = false case let .member(_, _, adminRights, bannedRights): if channel.hasPermission(.addAdmins) { canPromote = true } else { canPromote = false } if channel.hasPermission(.banMembers) { canRestrict = true } else { canRestrict = false } if canPromote { if let bannedRights = bannedRights { if bannedRights.restrictedBy != account.peerId && !channel.flags.contains(.isCreator) { canPromote = false } } } if canRestrict { if let adminRights = adminRights { if adminRights.promotedBy != account.peerId && !channel.flags.contains(.isCreator) { canRestrict = false } } } }*/ var label: String? var enabled = true if case .banAndPromoteActions = mode { if case .creator = participant.participant { label = themeAndStrings.1.Channel_Management_LabelOwner enabled = false } } else if case .searchMembers = mode { switch participant.participant { case .creator: label = themeAndStrings.1.Channel_Management_LabelOwner case let .member(member): if member.adminInfo != nil { label = themeAndStrings.1.Channel_Management_LabelEditor } } } if state.removingParticipantIds.contains(participant.peer.id) { enabled = false } var peerActions: [ParticipantRevealAction] = [] /*if case .searchMembers = mode { if canPromote { peerActions.append(ParticipantRevealAction(type: .neutral, title: themeAndStrings.1.GroupInfo_ActionPromote, action: .promote)) } if canRestrict { peerActions.append(ParticipantRevealAction(type: .warning, title: themeAndStrings.1.GroupInfo_ActionRestrict, action: .restrict)) peerActions.append(ParticipantRevealAction(type: .destructive, title: themeAndStrings.1.Common_Delete, action: .remove)) } }*/ switch mode { case .searchAdmins: switch participant.participant { case .creator: label = themeAndStrings.1.Channel_Management_LabelOwner case let .member(_, _, adminInfo, _): if let adminInfo = adminInfo { if let peer = participant.peers[adminInfo.promotedBy] { if peer.id == participant.peer.id { label = themeAndStrings.1.Channel_Management_LabelAdministrator } else { label = themeAndStrings.1.Channel_Management_PromotedBy(peer.displayTitle).0 } } } } case .searchBanned: switch participant.participant { case let .member(_, _, _, banInfo): if let banInfo = banInfo { var exceptionsString = "" for rights in allGroupPermissionList { if banInfo.rights.flags.contains(rights) { if !exceptionsString.isEmpty { exceptionsString.append(", ") } exceptionsString.append(compactStringForGroupPermission(strings: themeAndStrings.1, right: rights)) } } label = exceptionsString } default: break } case .searchKicked: switch participant.participant { case let .member(_, _, _, banInfo): if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { label = themeAndStrings.1.Channel_Management_RemovedBy(peer.displayTitle).0 } default: break } default: break } entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: peerActions, revealed: state.revealedPeerId == RevealedPeerId(peerId: participant.peer.id, section: section), enabled: enabled), section: section, dateTimeFormat: themeAndStrings.4)) index += 1 } } for participant in foundMembers { if !existingPeerIds.contains(participant.peer.id) { existingPeerIds.insert(participant.peer.id) let section: ChannelMembersSearchSection var addIcon = false switch mode { case .inviteActions, .banAndPromoteActions: section = .members case .searchBanned: section = .members addIcon = true case .searchMembers, .searchKicked, .searchAdmins: section = .none } var label: String? var enabled = true if case .banAndPromoteActions = mode { if case .creator = participant.participant { label = themeAndStrings.1.Channel_Management_LabelOwner enabled = false } } entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: [], revealed: false, enabled: enabled), section: section, dateTimeFormat: themeAndStrings.4, addIcon: addIcon)) index += 1 } } return entries } } else { return .single(nil) } } let previousSearchItems = Atomic<[ChannelMembersSearchEntry]?>(value: nil) let previousEmptyQueryItems = Atomic<[ChannelMembersSearchEntry]?>(value: nil) self.emptyQueryDisposable.set((combineLatest(emptyQueryItems, self.themeAndStringsPromise.get()) |> deliverOnMainQueue).start(next: { [weak self] entries, themeAndStrings in if let strongSelf = self { let previousEntries = previousEmptyQueryItems.swap(entries) let firstTime = previousEntries == nil let transition = channelMembersSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, account: context.account, theme: themeAndStrings.0, strings: themeAndStrings.1, nameSortOrder: themeAndStrings.2, nameDisplayOrder: themeAndStrings.3, interaction: interaction) strongSelf.enqueueEmptyQueryTransition(transition, firstTime: firstTime) if entries == nil { strongSelf.emptyQueryListNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) } else { strongSelf.emptyQueryListNode.backgroundColor = themeAndStrings.0.chatList.backgroundColor } } })) self.searchDisposable.set((combineLatest(foundItems, self.themeAndStringsPromise.get()) |> deliverOnMainQueue).start(next: { [weak self] entries, themeAndStrings in if let strongSelf = self { let previousEntries = previousSearchItems.swap(entries) updateActivity(false) let firstTime = previousEntries == nil let transition = channelMembersSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, account: context.account, theme: themeAndStrings.0, strings: themeAndStrings.1, nameSortOrder: themeAndStrings.2, nameDisplayOrder: themeAndStrings.3, interaction: interaction) strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) self.presentationDataDisposable = (context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme let previousStrings = strongSelf.presentationData.strings strongSelf.presentationData = presentationData if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) } } }) self.listNode.beganInteractiveDragging = { [weak self] in self?.dismissInput?() } } deinit { self.searchDisposable.dispose() self.presentationDataDisposable?.dispose() self.removeMemberDisposable.dispose() } override func didLoad() { super.didLoad() } private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.emptyQueryListNode.backgroundColor = theme.chatList.backgroundColor self.listNode.backgroundColor = theme.chatList.backgroundColor } override func searchTextUpdated(text: String) { if text.isEmpty { self.searchQuery.set(.single(nil)) } else { self.searchQuery.set(.single(text)) } } private func enqueueEmptyQueryTransition(_ transition: ChannelMembersSearchContainerTransition, firstTime: Bool) { enqueuedEmptyQueryTransitions.append((transition, firstTime)) if self.hasValidLayout { while !self.enqueuedEmptyQueryTransitions.isEmpty { self.dequeueEmptyQueryTransition() } } } private func enqueueTransition(_ transition: ChannelMembersSearchContainerTransition, firstTime: Bool) { enqueuedTransitions.append((transition, firstTime)) if self.hasValidLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } } } private func dequeueTransition() { if let (transition, firstTime) = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() options.insert(.PreferSynchronousDrawing) options.insert(.PreferSynchronousResourceLoading) if firstTime { } else { //options.insert(.AnimateAlpha) } let isSearching = transition.isSearching self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in self?.listNode.isHidden = !isSearching self?.emptyQueryListNode.isHidden = isSearching }) } } private func dequeueEmptyQueryTransition() { if let (transition, firstTime) = self.enqueuedEmptyQueryTransitions.first { self.enqueuedEmptyQueryTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() options.insert(.PreferSynchronousDrawing) options.insert(.PreferSynchronousResourceLoading) if firstTime { } else { //options.insert(.AnimateAlpha) } self.emptyQueryListNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in }) } } override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) var duration: Double = 0.0 var curve: UInt = 0 switch transition { case .immediate: break case let .animated(animationDuration, animationCurve): duration = animationDuration switch animationCurve { case .easeInOut, .custom: break case .spring: curve = 7 } } let listViewCurve: ListViewAnimationCurve if curve == 7 { listViewCurve = .Spring(duration: duration) } else { listViewCurve = .Default(duration: nil) } var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight insets.left += layout.safeInsets.left insets.right += layout.safeInsets.right self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.emptyQueryListNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.emptyQueryListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !hasValidLayout { hasValidLayout = true while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } } } override func scrollToTop() { if self.listNode.isHidden { self.emptyQueryListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else { self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } } @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.cancel?() } } }