Swiftgram/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift
2019-10-11 16:05:35 +04:00

1296 lines
71 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import AccountContext
import TemporaryCachedPeerDataManager
import SearchUI
import ItemListPeerItem
import ContactsPeerItem
import ChatListSearchItemHeader
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<GroupMembersSearchContextState>()
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<PeerId>()
}
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<String?>()
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, pushController: @escaping (ViewController) -> 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: { [weak self] peer, participant in
openPeer(peer, participant)
self?.listNode.clearHighlightAnimated(true)
}, 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
}
pushController(channelAdminController(context: context, peerId: peerId, adminId: participant.peer.id, initialParticipant: participant.participant, updated: { _ in
}, upgradedToSupergroup: { _, f in f() }, transferedOwnership: { _ in }))
}, restrictPeer: { participant in
updateState { state in
var state = state
state.revealedPeerId = nil
return state
}
pushController(channelBannedMemberController(context: context, peerId: peerId, memberId: participant.peer.id, initialParticipant: participant.participant, updated: { _ in
}, upgradedToSupergroup: { _, f in f() }))
}, 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<Bool, NoError> in
let result = ValuePromise<Bool>()
result.set(true)
return result.get()
}
|> mapToSignal { value -> Signal<Void, NoError> 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: []), rank: nil)
|> `catch` { _ -> Signal<Void, NoError> 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<Void, NoError> 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<PeerId>()
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(strings: themeAndStrings.1, displayOrder: themeAndStrings.3)).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(strings: themeAndStrings.1, displayOrder: themeAndStrings.3)).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>
let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), 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, rank: nil), 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, 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)
}
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([])
}
if mode == .banAndPromoteActions || mode == .inviteActions {
foundRemotePeers = .single(([], [])) |> then(searchPeers(account: context.account, query: query)
|> delay(0.2, queue: Queue.concurrentDefaultQueue()))
} else {
foundRemotePeers = .single(([], []))
}
return combineLatest(foundGroupMembers, foundMembers, foundRemotePeers, themeAndStringsPromise.get(), statePromise.get())
|> map { foundGroupMembers, foundMembers, foundRemotePeers, themeAndStrings, state -> [ChannelMembersSearchEntry]? in
var entries: [ChannelMembersSearchEntry] = []
var existingPeerIds = Set<PeerId>()
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(strings: themeAndStrings.1, displayOrder: themeAndStrings.3)).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(strings: themeAndStrings.1, displayOrder: themeAndStrings.3)).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 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 {
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?()
}
}
}