import Foundation import Display import SwiftSignalKit import Postbox import TelegramCore private final class ChannelAdminsControllerArguments { let account: Account let openRecentActions: () -> Void let updateCurrentAdministrationType: () -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let removeAdmin: (PeerId) -> Void let addAdmin: () -> Void let openAdmin: (ChannelParticipant) -> Void init(account: Account, openRecentActions: @escaping () -> Void, updateCurrentAdministrationType: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removeAdmin: @escaping (PeerId) -> Void, addAdmin: @escaping () -> Void, openAdmin: @escaping (ChannelParticipant) -> Void) { self.account = account self.openRecentActions = openRecentActions self.updateCurrentAdministrationType = updateCurrentAdministrationType self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removeAdmin = removeAdmin self.addAdmin = addAdmin self.openAdmin = openAdmin } } private enum ChannelAdminsSection: Int32 { case administration case admins } private enum ChannelAdminsEntryStableId: Hashable { case index(Int32) case peer(PeerId) var hashValue: Int { switch self { case let .index(index): return index.hashValue case let .peer(peerId): return peerId.hashValue } } static func ==(lhs: ChannelAdminsEntryStableId, rhs: ChannelAdminsEntryStableId) -> Bool { switch lhs { case let .index(index): if case .index(index) = rhs { return true } else { return false } case let .peer(peerId): if case .peer(peerId) = rhs { return true } else { return false } } } } private enum ChannelAdminsEntry: ItemListNodeEntry { case recentActions(PresentationTheme, String) case administrationType(PresentationTheme, String, String) case administrationInfo(PresentationTheme, String) case adminsHeader(PresentationTheme, String) case adminPeerItem(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Bool, Int32, RenderedChannelParticipant, ItemListPeerItemEditing, Bool) case addAdmin(PresentationTheme, String, Bool) case adminsInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { case .recentActions, .administrationType, .administrationInfo: return ChannelAdminsSection.administration.rawValue case .adminsHeader, .adminPeerItem, .addAdmin, .adminsInfo: return ChannelAdminsSection.admins.rawValue } } var stableId: ChannelAdminsEntryStableId { switch self { case .recentActions: return .index(0) case .administrationType: return .index(1) case .administrationInfo: return .index(2) case .adminsHeader: return .index(3) case .addAdmin: return .index(4) case .adminsInfo: return .index(5) case let .adminPeerItem(_, _, _, _, _, participant, _, _): return .peer(participant.peer.id) } } static func ==(lhs: ChannelAdminsEntry, rhs: ChannelAdminsEntry) -> Bool { switch lhs { case let .recentActions(lhsTheme, lhsText): if case let .recentActions(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .administrationType(lhsTheme, lhsText, lhsValue): if case let .administrationType(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .administrationInfo(lhsTheme, lhsText): if case let .administrationInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adminsHeader(lhsTheme, lhsText): if case let .adminsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .adminPeerItem(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsIsGroup, lhsIndex, lhsParticipant, lhsEditing, lhsEnabled): if case let .adminPeerItem(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsIsGroup, rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs { if lhsTheme !== rhsTheme { return false } if lhsStrings !== rhsStrings { return false } if lhsDateTimeFormat != rhsDateTimeFormat { return false } if lhsIsGroup != rhsIsGroup { return false } if lhsIndex != rhsIndex { return false } if lhsParticipant != rhsParticipant { return false } if lhsEditing != rhsEditing { return false } if lhsEnabled != rhsEnabled { return false } return true } else { return false } case let .adminsInfo(lhsTheme, lhsText): if case let .adminsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .addAdmin(lhsTheme, lhsText, lhsEditing): if case let .addAdmin(rhsTheme, rhsText, rhsEditing) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEditing == rhsEditing { return true } else { return false } } } static func <(lhs: ChannelAdminsEntry, rhs: ChannelAdminsEntry) -> Bool { switch lhs { case .recentActions: return true case .administrationType: switch rhs { case .recentActions: return false default: return true } case .administrationInfo: switch rhs { case .recentActions, .administrationType: return false default: return true } case .adminsHeader: switch rhs { case .recentActions, .administrationType, .administrationInfo: return false default: return true } case let .adminPeerItem(_, _, _, _, index, _, _, _): switch rhs { case .recentActions, .administrationType, .administrationInfo, .adminsHeader, .addAdmin: return false case let .adminPeerItem(_, _, _, _, rhsIndex, _, _, _): return index < rhsIndex default: return true } case .addAdmin: switch rhs { case .recentActions, .administrationType, .administrationInfo, .adminsHeader, .addAdmin: return false default: return true } case .adminsInfo: return false } } func item(_ arguments: ChannelAdminsControllerArguments) -> ListViewItem { switch self { case let .recentActions(theme, text): return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openRecentActions() }) case let .administrationType(theme, text, value): return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.updateCurrentAdministrationType() }) case let .administrationInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) case let .adminsHeader(theme, title): return ItemListSectionHeaderItem(theme: theme, text: title, sectionId: self.section) case let .adminPeerItem(theme, strings, dateTimeFormat, isGroup, _, participant, editing, enabled): let peerText: String let action: (() -> Void)? switch participant.participant { case .creator: peerText = strings.Channel_Management_LabelCreator action = nil case let .member(_, _, adminInfo, _): if let adminInfo = adminInfo { if let peer = participant.peers[adminInfo.promotedBy] { peerText = strings.Channel_Management_PromotedBy(peer.displayTitle).0 } else { peerText = "" } } else { peerText = "" } action = { arguments.openAdmin(participant.participant) } } return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: action, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removeAdmin(peerId) }) case let .addAdmin(theme, text, editing): return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, editing: editing, action: { arguments.addAdmin() }) case let .adminsInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } private enum CurrentAdministrationType { case everyoneCanAddMembers case adminsCanAddMembers } private struct ChannelAdminsControllerState: Equatable { let selectedType: CurrentAdministrationType? let editing: Bool let peerIdWithRevealedOptions: PeerId? let removingPeerId: PeerId? let removedPeerIds: Set let temporaryAdmins: [RenderedChannelParticipant] let searchingMembers: Bool init() { self.selectedType = nil self.editing = false self.peerIdWithRevealedOptions = nil self.removingPeerId = nil self.removedPeerIds = Set() self.temporaryAdmins = [] self.searchingMembers = false } init(selectedType: CurrentAdministrationType?, editing: Bool, peerIdWithRevealedOptions: PeerId?, removingPeerId: PeerId?, removedPeerIds: Set, temporaryAdmins: [RenderedChannelParticipant], searchingMembers: Bool) { self.selectedType = selectedType self.editing = editing self.peerIdWithRevealedOptions = peerIdWithRevealedOptions self.removingPeerId = removingPeerId self.removedPeerIds = removedPeerIds self.temporaryAdmins = temporaryAdmins self.searchingMembers = searchingMembers } static func ==(lhs: ChannelAdminsControllerState, rhs: ChannelAdminsControllerState) -> Bool { if lhs.selectedType != rhs.selectedType { return false } if lhs.editing != rhs.editing { return false } if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions { return false } if lhs.removingPeerId != rhs.removingPeerId { return false } if lhs.removedPeerIds != rhs.removedPeerIds { return false } if lhs.temporaryAdmins != rhs.temporaryAdmins { return false } if lhs.searchingMembers != rhs.searchingMembers { return false } return true } func withUpdatedSearchingMembers(_ searchingMembers: Bool) -> ChannelAdminsControllerState { return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins, searchingMembers: searchingMembers) } func withUpdatedSelectedType(_ selectedType: CurrentAdministrationType?) -> ChannelAdminsControllerState { return ChannelAdminsControllerState(selectedType: selectedType, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins, searchingMembers: self.searchingMembers) } func withUpdatedEditing(_ editing: Bool) -> ChannelAdminsControllerState { return ChannelAdminsControllerState(selectedType: self.selectedType, editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins, searchingMembers: self.searchingMembers) } func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> ChannelAdminsControllerState { return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins, searchingMembers: self.searchingMembers) } func withUpdatedRemovingPeerId(_ removingPeerId: PeerId?) -> ChannelAdminsControllerState { return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins, searchingMembers: self.searchingMembers) } func withUpdatedRemovedPeerIds(_ removedPeerIds: Set) -> ChannelAdminsControllerState { return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, removedPeerIds: removedPeerIds, temporaryAdmins: self.temporaryAdmins, searchingMembers: self.searchingMembers) } func withUpdatedTemporaryAdmins(_ temporaryAdmins: [RenderedChannelParticipant]) -> ChannelAdminsControllerState { return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: temporaryAdmins, searchingMembers: self.searchingMembers) } } private func channelAdminsControllerEntries(presentationData: PresentationData, accountPeerId: PeerId, view: PeerView, state: ChannelAdminsControllerState, participants: [RenderedChannelParticipant]?) -> [ChannelAdminsEntry] { var entries: [ChannelAdminsEntry] = [] if let peer = view.peers[view.peerId] as? TelegramChannel { var isGroup = false if case let .group(info) = peer.info { isGroup = true entries.append(.recentActions(presentationData.theme, presentationData.strings.Group_Info_AdminLog)) if peer.flags.contains(.isCreator) { let selectedType: CurrentAdministrationType if let current = state.selectedType { selectedType = current } else { if info.flags.contains(.everyMemberCanInviteMembers) { selectedType = .everyoneCanAddMembers } else { selectedType = .adminsCanAddMembers } } let selectedTypeValue: String let infoText: String switch selectedType { case .everyoneCanAddMembers: selectedTypeValue = presentationData.strings.ChannelMembers_WhoCanAddMembers_AllMembers infoText = presentationData.strings.ChannelMembers_WhoCanAddMembersAllHelp case .adminsCanAddMembers: selectedTypeValue = presentationData.strings.ChannelMembers_WhoCanAddMembers_Admins infoText = presentationData.strings.ChannelMembers_WhoCanAddMembersAdminsHelp } entries.append(.administrationType(presentationData.theme, presentationData.strings.ChannelMembers_WhoCanAddMembers, selectedTypeValue)) entries.append(.administrationInfo(presentationData.theme, infoText)) } } else { entries.append(.recentActions(presentationData.theme, presentationData.strings.Group_Info_AdminLog)) } if let participants = participants { entries.append(.adminsHeader(presentationData.theme, isGroup ? presentationData.strings.ChannelMembers_GroupAdminsTitle : presentationData.strings.ChannelMembers_ChannelAdminsTitle)) if peer.hasAdminRights(.canAddAdmins) { entries.append(.addAdmin(presentationData.theme, presentationData.strings.Channel_Management_AddModerator, state.editing)) } var combinedParticipants: [RenderedChannelParticipant] = participants var existingParticipantIds = Set() for participant in participants { existingParticipantIds.insert(participant.peer.id) } for participant in state.temporaryAdmins { if !existingParticipantIds.contains(participant.peer.id) { combinedParticipants.append(participant) } } var index: Int32 = 0 for participant in combinedParticipants.sorted(by: { lhs, rhs in let lhsInvitedAt: Int32 switch lhs.participant { case .creator: lhsInvitedAt = Int32.min case let .member(_, invitedAt, _, _): lhsInvitedAt = invitedAt } let rhsInvitedAt: Int32 switch rhs.participant { case .creator: rhsInvitedAt = Int32.min case let .member(_, invitedAt, _, _): rhsInvitedAt = invitedAt } return lhsInvitedAt < rhsInvitedAt }) { if !state.removedPeerIds.contains(participant.peer.id) { var editable = true switch participant.participant { case .creator: editable = false case let .member(id, _, adminInfo, _): if id == accountPeerId { editable = false } else if let adminInfo = adminInfo { if peer.flags.contains(.isCreator) || adminInfo.promotedBy == accountPeerId { editable = true } else { editable = false } } else { editable = false } } entries.append(.adminPeerItem(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, isGroup, index, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), existingParticipantIds.contains(participant.peer.id))) index += 1 } } if peer.hasAdminRights(.canAddAdmins) { let info = isGroup ? presentationData.strings.Group_Management_AddModeratorHelp : presentationData.strings.Channel_Management_AddModeratorHelp entries.append(.adminsInfo(presentationData.theme, info)) } } } return entries } public func channelAdminsController(account: Account, peerId: PeerId) -> ViewController { let statePromise = ValuePromise(ChannelAdminsControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: ChannelAdminsControllerState()) let updateState: ((ChannelAdminsControllerState) -> ChannelAdminsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? let actionsDisposable = DisposableSet() let updateAdministrationDisposable = MetaDisposable() actionsDisposable.add(updateAdministrationDisposable) let removeAdminDisposable = MetaDisposable() actionsDisposable.add(removeAdminDisposable) let addAdminDisposable = MetaDisposable() actionsDisposable.add(addAdminDisposable) let adminsPromise = Promise<[RenderedChannelParticipant]?>(nil) let presentationDataSignal = (account.applicationContext as! TelegramApplicationContext).presentationData let arguments = ChannelAdminsControllerArguments(account: account, openRecentActions: { let _ = (account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in pushControllerImpl?(ChatRecentActionsController(account: account, peer: peer)) }) }, updateCurrentAdministrationType: { let _ = (presentationDataSignal |> take(1) |> deliverOnMainQueue).start(next: { presentationData in let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) let result = ValuePromise() actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.ChannelMembers_WhoCanAddMembers_AllMembers, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() result.set(true) }), ActionSheetButtonItem(title: presentationData.strings.ChannelMembers_WhoCanAddMembers_Admins, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() result.set(false) }) ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) let updateSignal = result.get() |> take(1) |> mapToSignal { value -> Signal in updateState { state in return state.withUpdatedSelectedType(value ? .everyoneCanAddMembers : .adminsCanAddMembers) } return account.postbox.loadedPeerWithId(peerId) |> mapToSignal { peer -> Signal in if let peer = peer as? TelegramChannel, case let .group(info) = peer.info { var updatedValue: Bool? if value && !info.flags.contains(.everyMemberCanInviteMembers) { updatedValue = true } else if !value && info.flags.contains(.everyMemberCanInviteMembers) { updatedValue = false } if let updatedValue = updatedValue { return updateGroupManagementType(account: account, peerId: peerId, type: updatedValue ? .unrestricted : .restrictedToAdmins) |> `catch` { _ -> Signal in return .complete() } } else { return .complete() } } else { return .complete() } } } updateAdministrationDisposable.set(updateSignal.start()) presentControllerImpl?(actionSheet, nil) }) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { return state.withUpdatedPeerIdWithRevealedOptions(peerId) } else { return state } } }, removeAdmin: { adminId in updateState { return $0.withUpdatedRemovingPeerId(adminId) } removeAdminDisposable.set((account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: account, peerId: peerId, memberId: adminId, adminRights: TelegramChannelAdminRights(flags: [])) |> deliverOnMainQueue).start(completed: { updateState { return $0.withUpdatedRemovingPeerId(nil) } })) }, addAdmin: { updateState { current in presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, mode: .promote, filters: [], openPeer: { peer, participant in let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } if peer.id == account.peerId { return } if let participant = participant { switch participant.participant { case .creator: return case let .member(_, _, _, banInfo): if let banInfo = banInfo, banInfo.restrictedBy != account.peerId { presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Channel_Members_AddAdminErrorBlacklisted, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) return } } } presentControllerImpl?(channelAdminController(account: account, peerId: peerId, adminId: peer.id, initialParticipant: participant?.participant, updated: { _ in }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) return current } }, openAdmin: { participant in if case let .member(adminId, _, _, _) = participant { presentControllerImpl?(channelAdminController(account: account, peerId: peerId, adminId: participant.peerId, initialParticipant: participant, updated: { _ in }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } }) let peerView = account.viewTracker.peerView(peerId) |> deliverOnMainQueue let (membersDisposable, loadMoreControl) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: account.postbox, network: account.network, peerId: peerId) { membersState in if case .loading = membersState.loadingState, membersState.list.isEmpty { adminsPromise.set(.single(nil)) } else { adminsPromise.set(.single(membersState.list)) } } actionsDisposable.add(membersDisposable) var previousPeers: [RenderedChannelParticipant]? let signal = combineLatest(presentationDataSignal, statePromise.get(), peerView, adminsPromise.get() |> deliverOnMainQueue) |> deliverOnMainQueue |> map { presentationData, state, view, admins -> (ItemListControllerState, (ItemListNodeState, ChannelAdminsEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? var secondaryRightNavigationButton: ItemListNavigationButton? if let admins = admins, admins.count > 1 { if state.editing { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) } else if let peer = view.peers[peerId] as? TelegramChannel, peer.flags.contains(.isCreator) { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } }) } if !state.editing { if rightNavigationButton == nil { rightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedSearchingMembers(true) } }) } else { secondaryRightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedSearchingMembers(true) } }) } } _ = stateValue.swap(state.withUpdatedTemporaryAdmins(admins)) } let previous = previousPeers previousPeers = admins var isGroup = true if let peer = view.peers[peerId] as? TelegramChannel, case .broadcast = peer.info { isGroup = false } var searchItem: ItemListControllerSearch? if state.searchingMembers { searchItem = ChannelMembersSearchItem(account: account, peerId: peerId, searchMode: .searchAdmins, cancel: { updateState { state in return state.withUpdatedSearchingMembers(false) } }, openPeer: { _, participant in if let participant = participant?.participant, case .member = participant { presentControllerImpl?(channelAdminController(account: account, peerId: peerId, adminId: participant.peerId, initialParticipant: participant, updated: { _ in }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } }) } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(isGroup ? presentationData.strings.ChatAdmins_Title : presentationData.strings.Channel_Management_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let listState = ItemListNodeState(entries: channelAdminsControllerEntries(presentationData: presentationData, accountPeerId: account.peerId, view: view, state: state, participants: admins), style: .blocks, searchItem: searchItem, animateChanges: previous != nil && admins != nil && previous!.count >= admins!.count) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let controller = ItemListController(account: account, state: signal) pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window(.root), with: p) controller.view.endEditing(true) } } controller.visibleBottomContentOffsetChanged = { offset in if case let .known(value) = offset, value < 40.0 { account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl) } } return controller }