import Foundation import Display import SwiftSignalKit import Postbox import TelegramCore private final class ChatRecentActionsFilterControllerArguments { let account: Account let toggleAllActions: () -> Void let toggleAction: ([AdminLogEventsFlags]) -> Void let toggleAllAdmins: () -> Void let toggleAdmin: (PeerId) -> Void init(account: Account, toggleAllActions: @escaping () -> Void, toggleAction: @escaping ([AdminLogEventsFlags]) -> Void, toggleAllAdmins: @escaping () -> Void, toggleAdmin: @escaping (PeerId) -> Void) { self.account = account self.toggleAllActions = toggleAllActions self.toggleAction = toggleAction self.toggleAllAdmins = toggleAllAdmins self.toggleAdmin = toggleAdmin } } private enum ChatRecentActionsFilterSection: Int32 { case actions case admins } private enum ChatRecentActionsFilterEntryStableId: 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: ChatRecentActionsFilterEntryStableId, rhs: ChatRecentActionsFilterEntryStableId) -> 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 ChatRecentActionsFilterEntry: ItemListNodeEntry { case actionsTitle(PresentationTheme, String) case allActions(PresentationTheme, String, Bool) case actionItem(PresentationTheme, Int32, [AdminLogEventsFlags], String, Bool) case adminsTitle(PresentationTheme, String) case allAdmins(PresentationTheme, String, Bool) case adminPeerItem(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Int32, RenderedChannelParticipant, Bool) var section: ItemListSectionId { switch self { case .actionsTitle, .allActions, .actionItem: return ChatRecentActionsFilterSection.actions.rawValue case .adminsTitle, .allAdmins, .adminPeerItem: return ChatRecentActionsFilterSection.admins.rawValue } } var stableId: ChatRecentActionsFilterEntryStableId { switch self { case .actionsTitle: return .index(0) case .allActions: return .index(1) case let .actionItem(_, index, _, _, _): return .index(100 + index) case .adminsTitle: return .index(200) case .allAdmins: return .index(201) case let .adminPeerItem(_, _, _, _, participant, _): return .peer(participant.peer.id) } } static func ==(lhs: ChatRecentActionsFilterEntry, rhs: ChatRecentActionsFilterEntry) -> Bool { switch lhs { case let .actionsTitle(lhsTheme, lhsText): if case let .actionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .allActions(lhsTheme, lhsText, lhsValue): if case let .allActions(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .actionItem(lhsTheme, lhsIndex, lhsFlags, lhsText, lhsValue): if case let .actionItem(rhsTheme, rhsIndex, rhsFlags, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsFlags == rhsFlags, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .adminsTitle(lhsTheme, lhsText): if case let .adminsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .allAdmins(lhsTheme, lhsText, lhsValue): if case let .allAdmins(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .adminPeerItem(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsIndex, lhsParticipant, lhsChecked): if case let .adminPeerItem(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsIndex, rhsParticipant, rhsChecked) = rhs { if lhsTheme !== rhsTheme { return false } if lhsStrings !== rhsStrings { return false } if lhsDateTimeFormat != rhsDateTimeFormat { return false } if lhsIndex != rhsIndex { return false } if lhsParticipant != rhsParticipant { return false } if lhsChecked != rhsChecked { return false } return true } else { return false } } } static func <(lhs: ChatRecentActionsFilterEntry, rhs: ChatRecentActionsFilterEntry) -> Bool { switch lhs { case .actionsTitle: return true case .allActions: switch rhs { case .actionsTitle: return false default: return true } case let .actionItem(_, lhsIndex, _, _, _): switch rhs { case .actionsTitle, .allActions: return false case let .actionItem(_, rhsIndex, _, _, _): return lhsIndex < rhsIndex default: return true } case .adminsTitle: switch rhs { case .adminPeerItem, .allAdmins: return true default: return false } case .allAdmins: switch rhs { case .adminPeerItem: return true default: return false } case let .adminPeerItem(_, _, _, lhsIndex, _, _): switch rhs { case let .adminPeerItem(_, _, _, rhsIndex, _, _): return lhsIndex < rhsIndex default: return false } } } func item(_ arguments: ChatRecentActionsFilterControllerArguments) -> ListViewItem { switch self { case let .actionsTitle(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .allActions(theme, text, value): return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { _ in arguments.toggleAllActions() }) case let .actionItem(theme, _, events, text, value): return ItemListCheckboxItem(theme: theme, title: text, style: .right, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.toggleAction(events) }) case let .adminsTitle(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .allAdmins(theme, text, value): return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { _ in arguments.toggleAllAdmins() }) case let .adminPeerItem(theme, strings, dateTimeFormat, _, participant, checked): let peerText: String switch participant.participant { case .creator: peerText = strings.Channel_Management_LabelCreator case .member: peerText = strings.ChatAdmins_AdminLabel.capitalized } return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: checked, style: .check), enabled: true, sectionId: self.section, action: { arguments.toggleAdmin(participant.peer.id) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) } } } private struct ChatRecentActionsFilterControllerState: Equatable { let events: AdminLogEventsFlags let adminPeerIds: [PeerId]? init(events: AdminLogEventsFlags, adminPeerIds: [PeerId]?) { self.events = events self.adminPeerIds = adminPeerIds } static func ==(lhs: ChatRecentActionsFilterControllerState, rhs: ChatRecentActionsFilterControllerState) -> Bool { if lhs.events != rhs.events { return false } if let lhsAdminPeerIds = lhs.adminPeerIds, let rhsAdminPeerIds = rhs.adminPeerIds { if lhsAdminPeerIds != rhsAdminPeerIds { return false } } else if (lhs.adminPeerIds != nil) != (rhs.adminPeerIds != nil) { return false } return true } func withUpdatedEvents(_ events: AdminLogEventsFlags) -> ChatRecentActionsFilterControllerState { return ChatRecentActionsFilterControllerState(events: events, adminPeerIds: self.adminPeerIds) } func withUpdatedAdminPeerIds(_ adminPeerIds: [PeerId]?) -> ChatRecentActionsFilterControllerState { return ChatRecentActionsFilterControllerState(events: self.events, adminPeerIds: adminPeerIds) } } private func channelRecentActionsFilterControllerEntries(presentationData: PresentationData, accountPeerId: PeerId, peer: Peer, state: ChatRecentActionsFilterControllerState, participants: [RenderedChannelParticipant]?) -> [ChatRecentActionsFilterEntry] { var isGroup = true if let peer = peer as? TelegramChannel, case .broadcast = peer.info { isGroup = false } var entries: [ChatRecentActionsFilterEntry] = [] let order: [([AdminLogEventsFlags], String)] if isGroup { order = [ ([.ban, .unban], presentationData.strings.Channel_AdminLogFilter_EventsRestrictions), ([.promote, .demote], presentationData.strings.Channel_AdminLogFilter_EventsAdmins), ([.invite, .join], presentationData.strings.Channel_AdminLogFilter_EventsNewMembers), ([.info], isGroup ? presentationData.strings.Channel_AdminLogFilter_EventsInfo : presentationData.strings.Channel_AdminLogFilter_ChannelEventsInfo), ([.deleteMessages], presentationData.strings.Channel_AdminLogFilter_EventsDeletedMessages), ([.editMessages], presentationData.strings.Channel_AdminLogFilter_EventsEditedMessages), ([.pinnedMessages], presentationData.strings.Channel_AdminLogFilter_EventsPinned), ([.leave], presentationData.strings.Channel_AdminLogFilter_EventsLeaving), ] } else { order = [ ([.promote, .demote], presentationData.strings.Channel_AdminLogFilter_EventsAdmins), ([.invite, .join], presentationData.strings.Channel_AdminLogFilter_EventsNewMembers), ([.info], isGroup ? presentationData.strings.Channel_AdminLogFilter_EventsInfo : presentationData.strings.Channel_AdminLogFilter_ChannelEventsInfo), ([.deleteMessages], presentationData.strings.Channel_AdminLogFilter_EventsDeletedMessages), ([.editMessages], presentationData.strings.Channel_AdminLogFilter_EventsEditedMessages), ([.leave], presentationData.strings.Channel_AdminLogFilter_EventsLeaving), ] } var allTypesSelected = true outer: for (events, _) in order { for event in events { if !state.events.contains(event) { allTypesSelected = false break outer } } } entries.append(.actionsTitle(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_EventsTitle)) entries.append(.allActions(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_EventsAll, allTypesSelected)) var index: Int32 = 0 for (events, text) in order { var eventsSelected = true inner: for event in events { if !state.events.contains(event) { eventsSelected = false break inner } } entries.append(.actionItem(presentationData.theme, index, events, text, eventsSelected)) index += 1 } if let participants = participants { var allAdminsSelected = true if let adminPeerIds = state.adminPeerIds { for participant in participants { if !adminPeerIds.contains(participant.peer.id) { allAdminsSelected = false break } } } else { allAdminsSelected = true } entries.append(.adminsTitle(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_AdminsTitle)) entries.append(.allAdmins(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_AdminsAll, allAdminsSelected)) var index: Int32 = 0 for participant in participants { var adminSelected = true if let adminPeerIds = state.adminPeerIds { if !adminPeerIds.contains(participant.peer.id) { adminSelected = false } } else { adminSelected = true } entries.append(.adminPeerItem(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, index, participant, adminSelected)) index += 1 } } return entries } public func channelRecentActionsFilterController(account: Account, peer: Peer, events: AdminLogEventsFlags, adminPeerIds: [PeerId]?, apply: @escaping (_ events: AdminLogEventsFlags, _ adminPeerIds: [PeerId]?) -> Void) -> ViewController { let statePromise = ValuePromise(ChatRecentActionsFilterControllerState(events: events, adminPeerIds: adminPeerIds), ignoreRepeated: true) let stateValue = Atomic(value: ChatRecentActionsFilterControllerState(events: events, adminPeerIds: adminPeerIds)) let updateState: ((ChatRecentActionsFilterControllerState) -> ChatRecentActionsFilterControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var dismissImpl: (() -> Void)? let adminsPromise = Promise<[RenderedChannelParticipant]?>(nil) let presentationDataSignal = (account.applicationContext as! TelegramApplicationContext).presentationData let actionsDisposable = DisposableSet() let arguments = ChatRecentActionsFilterControllerArguments(account: account, toggleAllActions: { updateState { current in if current.events.isEmpty { return current.withUpdatedEvents(.all) } else { return current.withUpdatedEvents([]) } } }, toggleAction: { events in if let first = events.first { updateState { current in var updatedEvents = current.events if updatedEvents.contains(first) { for event in events { updatedEvents.remove(event) } } else { for event in events { updatedEvents.insert(event) } } return current.withUpdatedEvents(updatedEvents) } } }, toggleAllAdmins: { let _ = (adminsPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { admins in if let _ = admins { updateState { current in if let _ = current.adminPeerIds { return current.withUpdatedAdminPeerIds(nil) } else { return current.withUpdatedAdminPeerIds([]) } } } }) }, toggleAdmin: { adminId in let _ = (adminsPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { admins in if let admins = admins { updateState { current in if let adminPeerIds = current.adminPeerIds, let index = adminPeerIds.index(of: adminId) { var updatedAdminPeerIds = adminPeerIds updatedAdminPeerIds.remove(at: index) return current.withUpdatedAdminPeerIds(updatedAdminPeerIds) } else { var updatedAdminPeerIds = current.adminPeerIds ?? admins.map { $0.peer.id } if !updatedAdminPeerIds.contains(adminId) { updatedAdminPeerIds.append(adminId) } return current.withUpdatedAdminPeerIds(updatedAdminPeerIds) } } } }) }) adminsPromise.set(.single(nil)) let (membersDisposable, _) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: account.postbox, network: account.network, peerId: peer.id) { 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(), adminsPromise.get() |> deliverOnMainQueue) |> deliverOnMainQueue |> map { presentationData, state, admins -> (ItemListControllerState, (ItemListNodeState, ChatRecentActionsFilterEntry.ItemGenerationArguments)) in let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) let doneEnabled = !state.events.isEmpty let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: { var resultState: ChatRecentActionsFilterControllerState? updateState { current in resultState = current return current } if let resultState = resultState { apply(resultState.events, resultState.adminPeerIds) } dismissImpl?() }) var sortedAdmins: [RenderedChannelParticipant]? if let admins = admins { sortedAdmins = admins.filter { $0.peer.id == account.peerId } + admins.filter({ $0.peer.id != account.peerId }) } else { sortedAdmins = nil } let previous = previousPeers previousPeers = sortedAdmins let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ChatAdmins_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let listState = ItemListNodeState(entries: channelRecentActionsFilterControllerEntries(presentationData: presentationData, accountPeerId: account.peerId, peer: peer, state: state, participants: sortedAdmins), style: .blocks, animateChanges: previous != nil && admins != nil && previous!.count >= sortedAdmins!.count) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let controller = ItemListController(account: account, state: signal) dismissImpl = { [weak controller] in controller?.dismiss() } return controller }