diff --git a/TelegramUI/ChannelAdminsController.swift b/TelegramUI/ChannelAdminsController.swift index e53c2fa1dd..140cea39dc 100644 --- a/TelegramUI/ChannelAdminsController.swift +++ b/TelegramUI/ChannelAdminsController.swift @@ -473,7 +473,7 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon } var pushControllerImpl: ((ViewController) -> Void)? - var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? let actionsDisposable = DisposableSet() @@ -669,6 +669,8 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon presentControllerImpl?(channelAdminController(account: account, peerId: peerId, adminId: participant.peerId, initialParticipant: participant, updated: { _ in }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } + }, present: { c, a in + presentControllerImpl?(c, a) }) } diff --git a/TelegramUI/ChannelBlacklistController.swift b/TelegramUI/ChannelBlacklistController.swift index afa60eaa34..e69655b667 100644 --- a/TelegramUI/ChannelBlacklistController.swift +++ b/TelegramUI/ChannelBlacklistController.swift @@ -311,7 +311,7 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View statePromise.set(stateValue.modify { f($0) }) } - var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? let actionsDisposable = DisposableSet() @@ -477,6 +477,8 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View arguments.openPeerInfo(rendered.peer) } } + }, present: { c, a in + presentControllerImpl?(c, a) }) } diff --git a/TelegramUI/ChannelMemberCategoryListContext.swift b/TelegramUI/ChannelMemberCategoryListContext.swift index 7e6df18ec8..ec0de98429 100644 --- a/TelegramUI/ChannelMemberCategoryListContext.swift +++ b/TelegramUI/ChannelMemberCategoryListContext.swift @@ -379,9 +379,29 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor } } case let .recentSearch(query): - break - default: - break + if let updated = updated, isParticipantMember(updated.participant), updated.peer.indexName.matchesByTokens(query) { + var found = false + loop: for i in 0 ..< list.count { + if list[i].peer.id == updated.peer.id { + list[i] = updated + found = true + updatedList = true + break loop + } + } + if !found { + list.insert(updated, at: 0) + updatedList = true + } + } else if let previous = previous, isParticipantMember(previous) { + loop: for i in 0 ..< list.count { + if list[i].peer.id == previous.peerId { + list.remove(at: i) + updatedList = true + break loop + } + } + } } } if updatedList { diff --git a/TelegramUI/ChannelMembersController.swift b/TelegramUI/ChannelMembersController.swift index 10316c296d..5a732f4432 100644 --- a/TelegramUI/ChannelMembersController.swift +++ b/TelegramUI/ChannelMembersController.swift @@ -310,7 +310,7 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo statePromise.set(stateValue.modify { f($0) }) } - var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? let actionsDisposable = DisposableSet() @@ -444,6 +444,8 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo pushControllerImpl?(infoController) // arguments.pushController(infoController) } + }, present: { c, a in + presentControllerImpl?(c, a) }) } diff --git a/TelegramUI/ChannelMembersSearchContainerNode.swift b/TelegramUI/ChannelMembersSearchContainerNode.swift index a8341d1d23..6150a55a96 100644 --- a/TelegramUI/ChannelMembersSearchContainerNode.swift +++ b/TelegramUI/ChannelMembersSearchContainerNode.swift @@ -35,7 +35,7 @@ private enum ChannelMembersSearchSection { private enum ChannelMembersSearchContent: Equatable { case peer(Peer) - case participant(participant: RenderedChannelParticipant, label: String?, revealActions: [ParticipantRevealAction], enabled: Bool) + case participant(participant: RenderedChannelParticipant, label: String?, revealActions: [ParticipantRevealAction], revealed: Bool, enabled: Bool) static func ==(lhs: ChannelMembersSearchContent, rhs: ChannelMembersSearchContent) -> Bool { switch lhs { @@ -45,8 +45,8 @@ private enum ChannelMembersSearchContent: Equatable { } else { return false } - case let .participant(participant, label, revealActions, enabled): - if case .participant(participant, label, revealActions, enabled) = rhs { + case let .participant(participant, label, revealActions, revealed, enabled): + if case .participant(participant, label, revealActions, revealed, enabled) = rhs { return true } else { return false @@ -58,12 +58,28 @@ private enum ChannelMembersSearchContent: Equatable { switch self { case let .peer(peer): return peer.id - case let .participant(participant, _, _, _): + case let .participant(participant, _, _, _, _): return participant.peer.id } } } +private final class ChannelMembersSearchContainerInteraction { + let peerSelected: (Peer, RenderedChannelParticipant?) -> Void + let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void + let promotePeer: (RenderedChannelParticipant) -> Void + let restrictPeer: (RenderedChannelParticipant) -> Void + let removePeer: (PeerId) -> Void + + init(peerSelected: @escaping (Peer, RenderedChannelParticipant?) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> 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 final class ChannelMembersSearchEntry: Comparable, Identifiable { let index: Int let content: ChannelMembersSearchContent @@ -87,31 +103,40 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable { return lhs.index < rhs.index } - func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, peerSelected: @escaping (Peer, RenderedChannelParticipant?) -> Void) -> ListViewItem { + 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 - peerSelected(peer, nil) + interaction.peerSelected(peer, nil) }) - case let .participant(participant, label, revealActions, enabled): + case let .participant(participant, label, revealActions, revealed, enabled): + let status: ContactsPeerItemStatus + if let label = label { + status = .custom(label) + } 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: - //arguments.promotePeer(participant) + interaction.promotePeer(participant) break case .restrict: - //arguments.restrictPeer(participant) + interaction.restrictPeer(participant) break case .remove: - //arguments.removePeer(peer.id) + interaction.removePeer(participant.peer.id) break } })) } - return ContactsPeerItem(theme: theme, strings: strings, sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, account: account, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: participant.peer), status: .none, enabled: enabled, 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 - peerSelected(participant.peer, participant) + 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, 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(peerId, fromPeerId) }) } } @@ -123,16 +148,21 @@ struct ChannelMembersSearchContainerTransition { let isSearching: Bool } -private func channelMembersSearchContainerPreparedRecentTransition(from fromEntries: [ChannelMembersSearchEntry], to toEntries: [ChannelMembersSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, peerSelected: @escaping (Peer, RenderedChannelParticipant?) -> Void) -> ChannelMembersSearchContainerTransition { +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, peerSelected: peerSelected), 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, peerSelected: peerSelected), 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: PeerId? + var removingParticipantIds = Set() +} + final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNode { private let account: Account private let openPeer: (Peer, RenderedChannelParticipant?) -> Void @@ -150,9 +180,11 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + private let removeMemberDisposable = MetaDisposable() + private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder)> - init(account: Account, peerId: PeerId, mode: ChannelMembersSearchMode, filters: [ChannelMembersSearchFilter], openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void, updateActivity: @escaping(Bool)->Void) { + init(account: Account, peerId: PeerId, mode: ChannelMembersSearchMode, filters: [ChannelMembersSearchFilter], openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void, updateActivity: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.account = account self.openPeer = openPeer self.mode = mode @@ -173,194 +205,323 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod self.addSubnode(self.dimNode) self.addSubnode(self.listNode) - let themeAndStringsPromise = self.themeAndStringsPromise - let foundItems = searchQuery.get() - |> mapToSignal { query -> Signal<[ChannelMembersSearchEntry]?, NoError> in - updateActivity(true) - if let query = query, !query.isEmpty { - let foundGroupMembers: Signal<[RenderedChannelParticipant], NoError> - let foundMembers: Signal<[RenderedChannelParticipant], NoError> - - switch mode { - case .searchMembers, .banAndPromoteActions: - foundGroupMembers = Signal { subscriber in - let (disposable, _) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: account.postbox, network: account.network, accountPeerId: 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([]) - case .inviteActions: - foundGroupMembers = .single([]) - foundMembers = channelMembers(postbox: account.postbox, network: account.network, accountPeerId: account.peerId, peerId: peerId, category: .recent(.search(query))) - |> map { $0 ?? [] } - case .searchAdmins: - foundGroupMembers = Signal { subscriber in - let (disposable, _) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: account.postbox, network: account.network, accountPeerId: 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([]) - case .searchBanned: - foundGroupMembers = Signal { subscriber in - let (disposable, _) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.restrictedAndBanned(postbox: account.postbox, network: account.network, accountPeerId: 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 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 + present(channelAdminController(account: account, peerId: peerId, adminId: participant.peer.id, initialParticipant: participant.participant, updated: { _ in + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, restrictPeer: { participant in + present(channelBannedMemberController(account: account, peerId: peerId, memberId: participant.peer.id, initialParticipant: participant.participant, updated: { _ in + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, removePeer: { memberId in + let signal = 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 } - let foundContacts: Signal<([Peer], [PeerId: PeerPresence]), NoError> - let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> - switch mode { - case .inviteActions, .banAndPromoteActions: - foundContacts = account.postbox.searchContacts(query: query.lowercased()) - foundRemotePeers = .single(([], [])) |> then(searchPeers(account: account, query: query) - |> delay(0.2, queue: Queue.concurrentDefaultQueue())) - case .searchMembers, .searchBanned, .searchAdmins: - foundContacts = .single(([], [:])) - foundRemotePeers = .single(([], [])) + if peerId.namespace == Namespaces.Peer.CloudChannel { + return account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: account, peerId: peerId, memberId: memberId, bannedRights: TelegramChannelBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) + |> afterDisposed { + Queue.mainQueue().async { + updateState { state in + var state = state + state.removingParticipantIds.remove(memberId) + return state + } + } + } } - return combineLatest(foundGroupMembers, foundMembers, foundContacts, foundRemotePeers, themeAndStringsPromise.get()) - |> map { foundGroupMembers, foundMembers, foundContacts, foundRemotePeers, themeAndStrings -> [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(account.peerId) - case .searchMembers, .searchAdmins, .searchBanned: - 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 .searchMembers, .searchBanned, .searchAdmins: - section = .none - } - - var label: String? - var enabled = true - if case .banAndPromoteActions = mode { - if case .creator = participant.participant { - label = themeAndStrings.1.Channel_Management_LabelCreator - enabled = false - } - } - switch mode { - case .searchAdmins: - switch participant.participant { - case .creator: - label = themeAndStrings.1.Channel_Management_LabelCreator - case let .member(_, _, adminInfo, _): - if let adminInfo = adminInfo { - if let peer = participant.peers[adminInfo.promotedBy] { - label = themeAndStrings.1.Channel_Management_PromotedBy(peer.displayTitle).0 - } - } - } - case .searchBanned: - switch participant.participant { - case let .member(_, _, _, banInfo): - if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { - label = themeAndStrings.1.Channel_Management_RestrictedBy(peer.displayTitle).0 - } - default: - break - } - default: - break - } - entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: [], enabled: enabled), section: section)) - index += 1 - } - } - - for participant in foundMembers { - if !existingPeerIds.contains(participant.peer.id) { - existingPeerIds.insert(participant.peer.id) - let section: ChannelMembersSearchSection - switch mode { - case .inviteActions, .banAndPromoteActions: - section = .members - case .searchMembers, .searchBanned, .searchAdmins: - section = .none - } - - var label: String? - var enabled = true - if case .banAndPromoteActions = mode { - if case .creator = participant.participant { - label = themeAndStrings.1.Channel_Management_LabelCreator - enabled = false - } - } - - - entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: [], enabled: enabled), section: section)) - 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)) - 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)) - 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)) - index += 1 - } - } - - return entries + return removePeerMember(account: account, peerId: peerId, memberId: memberId) + |> deliverOnMainQueue + |> afterDisposed { + updateState { state in + var state = state + state.removingParticipantIds.remove(memberId) + return state + } } } else { - return .single(nil) + return .complete() } + } + removeMemberDisposable.set(signal.start()) + }) + + let themeAndStringsPromise = self.themeAndStringsPromise + let foundItems = combineLatest(searchQuery.get(), account.postbox.multiplePeersView([peerId]) |> take(1)) + |> mapToSignal { query, peerView -> Signal<[ChannelMembersSearchEntry]?, NoError> in + guard let channel = peerView.peers[peerId] as? TelegramChannel else { + return .single(nil) + } + updateActivity(true) + if let query = query, !query.isEmpty { + let foundGroupMembers: Signal<[RenderedChannelParticipant], NoError> + let foundMembers: Signal<[RenderedChannelParticipant], NoError> + + switch mode { + case .searchMembers, .banAndPromoteActions: + foundGroupMembers = Signal { subscriber in + let (disposable, _) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: account.postbox, network: account.network, accountPeerId: 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: account.postbox, network: account.network, accountPeerId: account.peerId, peerId: peerId, category: .recent(.search(query))) + |> map { $0 ?? [] } + case .searchAdmins: + foundGroupMembers = Signal { subscriber in + let (disposable, _) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: account.postbox, network: account.network, accountPeerId: 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([]) + case .searchBanned: + foundGroupMembers = Signal { subscriber in + let (disposable, _) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.restrictedAndBanned(postbox: account.postbox, network: account.network, accountPeerId: 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 = account.postbox.searchContacts(query: query.lowercased()) + foundRemotePeers = .single(([], [])) |> then(searchPeers(account: account, query: query) + |> delay(0.2, queue: Queue.concurrentDefaultQueue())) + case .searchMembers, .searchBanned, .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(account.peerId) + case .searchMembers, .searchAdmins, .searchBanned: + 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 .searchMembers, .searchBanned, .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.hasAdminRights([.canAddAdmins]) { + canPromote = true + } else { + canPromote = false + } + if channel.hasAdminRights([.canBanUsers]) { + 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_LabelCreator + enabled = false + } + } else if case .searchMembers = mode { + switch participant.participant { + case .creator: + label = themeAndStrings.1.Channel_Management_LabelCreator + 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_LabelCreator + case let .member(_, _, adminInfo, _): + if let adminInfo = adminInfo { + if let peer = participant.peers[adminInfo.promotedBy] { + label = themeAndStrings.1.Channel_Management_PromotedBy(peer.displayTitle).0 + } + } + } + case .searchBanned: + switch participant.participant { + case let .member(_, _, _, banInfo): + if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { + label = themeAndStrings.1.Channel_Management_RestrictedBy(peer.displayTitle).0 + } + default: + break + } + default: + break + } + entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: peerActions, revealed: state.revealedPeerId == participant.peer.id, enabled: enabled), section: section)) + index += 1 + } + } + + for participant in foundMembers { + if !existingPeerIds.contains(participant.peer.id) { + existingPeerIds.insert(participant.peer.id) + let section: ChannelMembersSearchSection + switch mode { + case .inviteActions, .banAndPromoteActions: + section = .members + case .searchMembers, .searchBanned, .searchAdmins: + section = .none + } + + var label: String? + var enabled = true + if case .banAndPromoteActions = mode { + if case .creator = participant.participant { + label = themeAndStrings.1.Channel_Management_LabelCreator + enabled = false + } + } + + + entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: [], revealed: false, enabled: enabled), section: section)) + 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)) + 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)) + 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)) + index += 1 + } + } + + return entries + } + } else { + return .single(nil) + } } let previousSearchItems = Atomic<[ChannelMembersSearchEntry]?>(value: nil) @@ -371,24 +532,24 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod let previousEntries = previousSearchItems.swap(entries) updateActivity(false) let firstTime = previousEntries == nil - let transition = channelMembersSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, account: account, theme: themeAndStrings.0, strings: themeAndStrings.1, nameSortOrder: themeAndStrings.2, nameDisplayOrder: themeAndStrings.3, peerSelected: openPeer) + let transition = channelMembersSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, account: account, theme: themeAndStrings.0, strings: themeAndStrings.1, nameSortOrder: themeAndStrings.2, nameDisplayOrder: themeAndStrings.3, interaction: interaction) strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) self.presentationDataDisposable = (account.telegramApplicationContext.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) - } + |> 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?() @@ -398,6 +559,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod deinit { self.searchDisposable.dispose() self.presentationDataDisposable?.dispose() + self.removeMemberDisposable.dispose() } override func didLoad() { diff --git a/TelegramUI/ChannelMembersSearchController.swift b/TelegramUI/ChannelMembersSearchController.swift index d7e4cde8f7..8c81ddea88 100644 --- a/TelegramUI/ChannelMembersSearchController.swift +++ b/TelegramUI/ChannelMembersSearchController.swift @@ -70,6 +70,9 @@ final class ChannelMembersSearchController: ViewController { self?.dismiss() self?.openPeer(peer, participant) } + self.controllerNode.present = { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + } self.displayNodeDidLoad() } diff --git a/TelegramUI/ChannelMembersSearchControllerNode.swift b/TelegramUI/ChannelMembersSearchControllerNode.swift index 9a39e7a9a5..50fc68e8b7 100644 --- a/TelegramUI/ChannelMembersSearchControllerNode.swift +++ b/TelegramUI/ChannelMembersSearchControllerNode.swift @@ -121,6 +121,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { var requestActivateSearch: (() -> Void)? var requestDeactivateSearch: (() -> Void)? var requestOpenPeerFromSearch: ((Peer, RenderedChannelParticipant?) -> Void)? + var present: ((ViewController, Any?) -> Void)? var themeAndStrings: (PresentationTheme, PresentationStrings) @@ -292,6 +293,8 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { self?.requestOpenPeerFromSearch?(peer, participant) }, updateActivity: { value in + }, present: { [weak self] c, a in + self?.present?(c, a) }), cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { requestDeactivateSearch() diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index f1e4acff77..69814b87df 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -115,6 +115,7 @@ class ContactsPeerItem: ListViewItem { let enabled: Bool let selection: ContactsPeerItemSelection let editing: ContactsPeerItemEditing + let options: [ItemListPeerItemRevealOption] let action: (ContactsPeerItemPeer) -> Void let setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? let deletePeer: ((PeerId) -> Void)? @@ -125,7 +126,7 @@ class ContactsPeerItem: ListViewItem { let header: ListViewItemHeader? - init(theme: PresentationTheme, strings: PresentationStrings, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, account: Account, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil) { + init(theme: PresentationTheme, strings: PresentationStrings, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, account: Account, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, options: [ItemListPeerItemRevealOption] = [], index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil) { self.theme = theme self.strings = strings self.sortOrder = sortOrder @@ -138,6 +139,7 @@ class ContactsPeerItem: ListViewItem { self.enabled = enabled self.selection = selection self.editing = editing + self.options = options self.action = action self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.deletePeer = deletePeer @@ -562,6 +564,32 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 13.0), size: titleLayout.size) } + let peerRevealOptions: [ItemListRevealOption] + if item.enabled { + var mappedOptions: [ItemListRevealOption] = [] + var index: Int32 = 0 + for option in item.options { + let color: UIColor + let textColor: UIColor + switch option.type { + case .neutral: + color = item.theme.list.itemDisclosureActions.constructive.fillColor + textColor = item.theme.list.itemDisclosureActions.constructive.foregroundColor + case .warning: + color = item.theme.list.itemDisclosureActions.warning.fillColor + textColor = item.theme.list.itemDisclosureActions.warning.foregroundColor + case .destructive: + color = item.theme.list.itemDisclosureActions.destructive.fillColor + textColor = item.theme.list.itemDisclosureActions.destructive.foregroundColor + } + mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor)) + index += 1 + } + peerRevealOptions = mappedOptions + } else { + peerRevealOptions = [] + } + return (nodeLayout, { [weak self] in if let strongSelf = self { return (.complete(), { [weak strongSelf] animated, synchronousLoads in @@ -710,12 +738,12 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - if item.editing.editable { - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) - strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) } else { - strongSelf.setRevealOptions((left: [], right: [])) + strongSelf.setRevealOptions((left: [], right: peerRevealOptions)) + strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) } } }) @@ -800,13 +828,17 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { if let item = self.item { - switch item.peer { - case let .peer(peer, chatPeer): - if let peer = chatPeer ?? peer { - item.deletePeer?(peer.id) - } - case .deviceContact: - break + if item.editing.editable { + switch item.peer { + case let .peer(peer, chatPeer): + if let peer = chatPeer ?? peer { + item.deletePeer?(peer.id) + } + case .deviceContact: + break + } + } else { + item.options[Int(option.key)].action() } } diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index 96201232dc..ed3e17acef 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -1780,6 +1780,8 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl if let infoController = peerInfoController(account: account, peer: peer) { arguments.pushController(infoController) } + }, present: { c, a in + presentControllerImpl?(c, a) }) } diff --git a/TelegramUI/GroupInfoSearchItem.swift b/TelegramUI/GroupInfoSearchItem.swift index 99b3df5c34..65ba371924 100644 --- a/TelegramUI/GroupInfoSearchItem.swift +++ b/TelegramUI/GroupInfoSearchItem.swift @@ -10,15 +10,19 @@ final class ChannelMembersSearchItem: ItemListControllerSearch { let peerId: PeerId let cancel: () -> Void let openPeer: (Peer, RenderedChannelParticipant?) -> Void + let present: (ViewController, Any?) -> Void let searchMode: ChannelMembersSearchMode + private var updateActivity: ((Bool) -> Void)? private var activity: ValuePromise = ValuePromise(ignoreRepeated: false) private let activityDisposable = MetaDisposable() - init(account: Account, peerId: PeerId, searchMode: ChannelMembersSearchMode = .searchMembers, cancel: @escaping () -> Void, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void) { + + init(account: Account, peerId: PeerId, searchMode: ChannelMembersSearchMode = .searchMembers, cancel: @escaping () -> Void, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.account = account self.peerId = peerId self.cancel = cancel self.openPeer = openPeer + self.present = present self.searchMode = searchMode activityDisposable.set((activity.get() |> mapToSignal { value -> Signal in if value { @@ -32,7 +36,7 @@ final class ChannelMembersSearchItem: ItemListControllerSearch { } deinit { - activityDisposable.dispose() + self.activityDisposable.dispose() } func isEqual(to: ItemListControllerSearch) -> Bool { @@ -63,6 +67,8 @@ final class ChannelMembersSearchItem: ItemListControllerSearch { func node(current: ItemListControllerSearchNode?) -> ItemListControllerSearchNode { return ChannelMembersSearchItemNode(account: self.account, peerId: self.peerId, searchMode: self.searchMode, openPeer: self.openPeer, cancel: self.cancel, updateActivity: { [weak self] value in self?.activity.set(value) + }, present: { [weak self] c, a in + self?.present(c, a) }) } } @@ -70,10 +76,10 @@ final class ChannelMembersSearchItem: ItemListControllerSearch { private final class ChannelMembersSearchItemNode: ItemListControllerSearchNode { private let containerNode: ChannelMembersSearchContainerNode - init(account: Account, peerId: PeerId, searchMode: ChannelMembersSearchMode, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool)->Void) { + init(account: Account, peerId: PeerId, searchMode: ChannelMembersSearchMode, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.containerNode = ChannelMembersSearchContainerNode(account: account, peerId: peerId, mode: searchMode, filters: [], openPeer: { peer, participant in openPeer(peer, participant) - }, updateActivity: updateActivity) + }, updateActivity: updateActivity, present: present) self.containerNode.cancel = { cancel() }