import Foundation import UIKit import AsyncDisplayKit import Display import Postbox import TelegramCore import SyncCore import SwiftSignalKit import TelegramPresentationData import TelegramUIPreferences import MergeLists import AccountContext import TemporaryCachedPeerDataManager import SearchBarNode import ContactsPeerItem import SearchUI import ItemListUI private final class ChannelMembersSearchInteraction { let openPeer: (Peer, RenderedChannelParticipant?) -> Void init(openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void) { self.openPeer = openPeer } } private enum ChannelMembersSearchEntryId: Hashable { case peer(PeerId) } private enum ChannelMembersSearchEntry: Comparable, Identifiable { case peer(Int, RenderedChannelParticipant, ContactsPeerItemEditing, String?, Bool) var stableId: ChannelMembersSearchEntryId { switch self { case let .peer(peer): return .peer(peer.1.peer.id) } } static func ==(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool { switch lhs { case let .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled): if case .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled) = rhs { return true } else { return false } } } static func <(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool { switch lhs { case let .peer(lhsPeer): if case let .peer(rhsPeer) = rhs { return lhsPeer.0 < rhsPeer.0 } else { return false } } } func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchInteraction) -> ListViewItem { switch self { case let .peer(_, participant, editing, label, enabled): let status: ContactsPeerItemStatus if let label = label { status = .custom(string: label, multiline: false) } else { status = .none } return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: nil), status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: nil, action: { _ in interaction.openPeer(participant.peer, participant) }) } } } private struct ChannelMembersSearchTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] let initial: Bool } private func preparedTransition(from fromEntries: [ChannelMembersSearchEntry]?, to toEntries: [ChannelMembersSearchEntry], context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchInteraction) -> ChannelMembersSearchTransition { 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(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } return ChannelMembersSearchTransition(deletions: deletions, insertions: insertions, updates: updates, initial: fromEntries == nil) } class ChannelMembersSearchControllerNode: ASDisplayNode { private let context: AccountContext private let peerId: PeerId private let mode: ChannelMembersSearchControllerMode private let filters: [ChannelMembersSearchFilter] let listNode: ListView var navigationBar: NavigationBar? private var enqueuedTransitions: [ChannelMembersSearchTransition] = [] private(set) var searchDisplayController: SearchDisplayController? private var containerLayout: (ContainerViewLayout, CGFloat)? var requestActivateSearch: (() -> Void)? var requestDeactivateSearch: (() -> Void)? var requestOpenPeerFromSearch: ((Peer, RenderedChannelParticipant?) -> Void)? var pushController: ((ViewController) -> Void)? private let forceTheme: PresentationTheme? var presentationData: PresentationData private var disposable: Disposable? private var listControl: PeerChannelMemberCategoryControl? init(context: AccountContext, presentationData: PresentationData, forceTheme: PresentationTheme?, peerId: PeerId, mode: ChannelMembersSearchControllerMode, filters: [ChannelMembersSearchFilter]) { self.context = context self.listNode = ListView() self.peerId = peerId self.mode = mode self.filters = filters self.presentationData = presentationData self.forceTheme = forceTheme if let forceTheme = forceTheme { self.presentationData = self.presentationData.withUpdated(theme: forceTheme) } super.init() self.setViewBlock({ return UITracingLayerView() }) self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.addSubnode(self.listNode) let interaction = ChannelMembersSearchInteraction(openPeer: { [weak self] peer, participant in self?.requestOpenPeerFromSearch?(peer, participant) self?.listNode.clearHighlightAnimated(true) }) let previousEntries = Atomic<[ChannelMembersSearchEntry]?>(value: nil) let disposableAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?) if peerId.namespace == Namespaces.Peer.CloudGroup { let disposable = (context.account.postbox.peerView(id: peerId) |> deliverOnMainQueue).start(next: { [weak self] peerView in guard let strongSelf = self else { return } guard let cachedData = peerView.cachedData as? CachedGroupData, let participants = cachedData.participants else { return } var creatorPeer: Peer? for participant in participants.participants { if let peer = peerView.peers[participant.peerId] { switch participant { case .creator: creatorPeer = peer default: break } } } guard let creator = creatorPeer else { return } var entries: [ChannelMembersSearchEntry] = [] var index = 0 for participant in participants.participants { guard let peer = peerView.peers[participant.peerId] else { continue } if peer.isDeleted { continue } var label: String? var enabled = true switch mode { case .ban: if peer.id == context.account.peerId { continue } for filter in filters { switch filter { case let .exclude(ids): if ids.contains(peer.id) { continue } case let .disable(ids): if ids.contains(peer.id) { enabled = false } case .excludeNonMembers: break } } case .promote: if peer.id == context.account.peerId { continue } for filter in filters { switch filter { case let .exclude(ids): if ids.contains(peer.id) { continue } case let .disable(ids): if ids.contains(peer.id) { enabled = false } case .excludeNonMembers: break } } if case .creator = participant { label = strongSelf.presentationData.strings.Channel_Management_LabelOwner enabled = false } case .inviteToCall: if peer.id == context.account.peerId { continue } for filter in filters { switch filter { case let .exclude(ids): if ids.contains(peer.id) { continue } case let .disable(ids): if ids.contains(peer.id) { enabled = false } case .excludeNonMembers: break } } } let renderedParticipant: RenderedChannelParticipant switch participant { case .creator: renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer) case .admin: var peers: [PeerId: Peer] = [:] peers[creator.id] = creator peers[peer.id] = peer renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(flags: .groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: 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, rank: nil), peer: peer, peers: peers) } entries.append(.peer(index, renderedParticipant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled)) index += 1 } let previous = previousEntries.swap(entries) strongSelf.enqueueTransition(preparedTransition(from: previous, to: entries, context: context, presentationData: strongSelf.presentationData, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, interaction: interaction)) }) disposableAndLoadMoreControl = (disposable, nil) } else { disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { [weak self] state in guard let strongSelf = self else { return } var entries: [ChannelMembersSearchEntry] = [] var index = 0 for participant in state.list { if participant.peer.isDeleted { continue } var label: String? var enabled = true switch mode { case .ban: if participant.peer.id == context.account.peerId { continue } for filter in filters { switch filter { case let .exclude(ids): if ids.contains(participant.peer.id) { continue } case let .disable(ids): if ids.contains(participant.peer.id) { enabled = false } case .excludeNonMembers: break } } case .promote: if participant.peer.id == context.account.peerId { continue } for filter in filters { switch filter { case let .exclude(ids): if ids.contains(participant.peer.id) { continue } case let .disable(ids): if ids.contains(participant.peer.id) { enabled = false } case .excludeNonMembers: break } } if case .creator = participant.participant { label = strongSelf.presentationData.strings.Channel_Management_LabelOwner enabled = false } case .inviteToCall: if participant.peer.id == context.account.peerId { continue } for filter in filters { switch filter { case let .exclude(ids): if ids.contains(participant.peer.id) { continue } case let .disable(ids): if ids.contains(participant.peer.id) { enabled = false } case .excludeNonMembers: break } } } entries.append(.peer(index, participant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled)) index += 1 } let previous = previousEntries.swap(entries) strongSelf.enqueueTransition(preparedTransition(from: previous, to: entries, context: context, presentationData: strongSelf.presentationData, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, interaction: interaction)) }) } self.disposable = disposableAndLoadMoreControl.0 self.listControl = disposableAndLoadMoreControl.1 if peerId.namespace == Namespaces.Peer.CloudChannel { self.listNode.visibleBottomContentOffsetChanged = { offset in if case let .known(value) = offset, value < 40.0 { context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: disposableAndLoadMoreControl.1) } } } self.listNode.beganInteractiveDragging = { [weak self] in self?.view.endEditing(true) } } deinit { self.disposable?.dispose() } func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData if let forceTheme = forceTheme { self.presentationData = self.presentationData.withUpdated(theme: forceTheme) } self.searchDisplayController?.updatePresentationData(self.presentationData) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { let hadValidLayout = self.containerLayout != nil self.containerLayout = (layout, navigationBarHeight) var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } } } func activateSearch(placeholderNode: SearchBarPlaceholderNode) { guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else { return } self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ChannelMembersSearchContainerNode(context: self.context, forceTheme: self.forceTheme, peerId: self.peerId, mode: .banAndPromoteActions, filters: self.filters, searchContext: nil, openPeer: { [weak self] peer, participant in self?.requestOpenPeerFromSearch?(peer, participant) }, updateActivity: { value in }, pushController: { [weak self] c in self?.pushController?(c) }), cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { requestDeactivateSearch() } }) self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in if let strongSelf = self, let strongPlaceholderNode = placeholderNode { if isSearchBar { strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode) } else { strongSelf.insertSubnode(subnode, belowSubnode: navigationBar) } } }, placeholder: placeholderNode) } func deactivateSearch(placeholderNode: SearchBarPlaceholderNode, animated: Bool) { if let searchDisplayController = self.searchDisplayController { searchDisplayController.deactivate(placeholder: placeholderNode) self.searchDisplayController = nil } } func animateIn() { self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } func animateOut(completion: (() -> Void)? = nil) { self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in completion?() }) } private func enqueueTransition(_ transition: ChannelMembersSearchTransition) { enqueuedTransitions.append(transition) if self.containerLayout != nil { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } } } private func dequeueTransition() { if let transition = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) let options = ListViewDeleteAndInsertOptions() if transition.initial { //options.insert(.Synchronous) //options.insert(.LowLatency) } else { //options.insert(.AnimateTopItemPosition) //options.insert(.AnimateCrossfade) } self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in }) } } func scrollToTop() { 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 }) } }