import Foundation import UIKit import Display import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import ViewControllerComponent import ComponentDisplayAdapters import TelegramPresentationData import AccountContext import TelegramCore import MultilineTextComponent import ButtonComponent import PresentationDataUtils import Markdown import UndoUI import TelegramStringFormatting import ListSectionComponent import ListActionItemComponent import PlainButtonComponent private enum ActionTypeSection: Hashable, CaseIterable { case members case settings case messages } private enum MembersActionType: Hashable, CaseIterable { case newAdminRights case newExceptions case newMembers case leftMembers func title(isGroup: Bool, strings: PresentationStrings) -> String { switch self { case .newAdminRights: return strings.Channel_AdminLogFilter_EventsAdminRights case .newExceptions: return strings.Channel_AdminLogFilter_EventsExceptions case .newMembers: return isGroup ? strings.Channel_AdminLogFilter_EventsNewMembers : strings.Channel_AdminLogFilter_EventsNewSubscribers case .leftMembers: return isGroup ? strings.Channel_AdminLogFilter_EventsLeavingGroup : strings.Channel_AdminLogFilter_EventsLeavingChannel } } var eventFlags: AdminLogEventsFlags { switch self { case .newAdminRights: return [.promote, .demote] case .newExceptions: return [.ban, .unban, .kick, .unkick] case .newMembers: return [.invite, .join] case .leftMembers: return [.leave] } } static func actionTypesFromFlags(_ eventFlags: AdminLogEventsFlags) -> [Self] { var actionTypes: [Self] = [] for actionType in Self.allCases { if !actionType.eventFlags.intersection(eventFlags).isEmpty { actionTypes.append(actionType) } } return actionTypes } } private enum SettingsActionType: Hashable, CaseIterable { case groupInfo case inviteLinks case videoChats func title(isGroup: Bool, strings: PresentationStrings) -> String { switch self { case .groupInfo: return isGroup ? strings.Channel_AdminLogFilter_EventsInfo : strings.Channel_AdminLogFilter_ChannelEventsInfo case .inviteLinks: return strings.Channel_AdminLogFilter_EventsInviteLinks case .videoChats: return isGroup ? strings.Channel_AdminLogFilter_EventsCalls : strings.Channel_AdminLogFilter_EventsLiveStreams } } var eventFlags: AdminLogEventsFlags { switch self { case .groupInfo: return [.info, .settings] case .inviteLinks: return [.invites] case .videoChats: return [.calls] } } static func actionTypesFromFlags(_ eventFlags: AdminLogEventsFlags) -> [Self] { var actionTypes: [Self] = [] for actionType in Self.allCases { if !actionType.eventFlags.intersection(eventFlags).isEmpty { actionTypes.append(actionType) } } return actionTypes } } private enum MessagesActionType: Hashable, CaseIterable { case deletedMessages case editedMessages case pinnedMessages func title(strings: PresentationStrings) -> String { switch self { case .deletedMessages: return strings.Channel_AdminLogFilter_EventsDeletedMessages case .editedMessages: return strings.Channel_AdminLogFilter_EventsEditedMessages case .pinnedMessages: return strings.Channel_AdminLogFilter_EventsPinned } } var eventFlags: AdminLogEventsFlags { switch self { case .deletedMessages: return [.deleteMessages] case .editedMessages: return [.editMessages] case .pinnedMessages: return [.pinnedMessages] } } static func actionTypesFromFlags(_ eventFlags: AdminLogEventsFlags) -> [Self] { var actionTypes: [Self] = [] for actionType in Self.allCases { if !actionType.eventFlags.intersection(eventFlags).isEmpty { actionTypes.append(actionType) } } return actionTypes } } private enum ActionType: Hashable { case members(MembersActionType) case settings(SettingsActionType) case messages(MessagesActionType) func title(isGroup: Bool, strings: PresentationStrings) -> String { switch self { case let .members(value): return value.title(isGroup: isGroup, strings: strings) case let .settings(value): return value.title(isGroup: isGroup, strings: strings) case let .messages(value): return value.title(strings: strings) } } } private final class RecentActionsSettingsSheetComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peer: EnginePeer let adminPeers: [EnginePeer] let initialValue: RecentActionsSettingsSheet.Value let completion: (RecentActionsSettingsSheet.Value) -> Void init( context: AccountContext, peer: EnginePeer, adminPeers: [EnginePeer], initialValue: RecentActionsSettingsSheet.Value, completion: @escaping (RecentActionsSettingsSheet.Value) -> Void ) { self.context = context self.peer = peer self.adminPeers = adminPeers self.initialValue = initialValue self.completion = completion } static func ==(lhs: RecentActionsSettingsSheetComponent, rhs: RecentActionsSettingsSheetComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.peer != rhs.peer { return false } if lhs.adminPeers != rhs.adminPeers { return false } return true } private struct ItemLayout: Equatable { var containerSize: CGSize var containerInset: CGFloat var bottomInset: CGFloat var topInset: CGFloat init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { self.containerSize = containerSize self.containerInset = containerInset self.bottomInset = bottomInset self.topInset = topInset } } private final class ScrollView: UIScrollView { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return super.hitTest(point, with: event) } } final class View: UIView, UIScrollViewDelegate { private let dimView: UIView private let backgroundLayer: SimpleLayer private let navigationBarContainer: SparseContainerView private let navigationBackgroundView: BlurredBackgroundView private let navigationBarSeparator: SimpleLayer private let scrollView: ScrollView private let scrollContentClippingView: SparseContainerView private let scrollContentView: UIView private let leftButton = ComponentView() private let title = ComponentView() private let actionButton = ComponentView() private let optionsSection = ComponentView() private let adminsSection = ComponentView() private let bottomOverscrollLimit: CGFloat private var ignoreScrolling: Bool = false private var component: RecentActionsSettingsSheetComponent? private weak var state: EmptyComponentState? private var environment: ViewControllerComponentContainer.Environment? private var isUpdating: Bool = false private var itemLayout: ItemLayout? private var topOffsetDistance: CGFloat? private var expandedSections = Set() private var selectedMembersActions = Set() private var selectedSettingsActions = Set() private var selectedMessagesActions = Set() private var selectedAdmins = Set() override init(frame: CGRect) { self.bottomOverscrollLimit = 200.0 self.dimView = UIView() self.backgroundLayer = SimpleLayer() self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] self.backgroundLayer.cornerRadius = 10.0 self.navigationBarContainer = SparseContainerView() self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) self.navigationBarSeparator = SimpleLayer() self.scrollView = ScrollView() self.scrollContentClippingView = SparseContainerView() self.scrollContentClippingView.clipsToBounds = true self.scrollContentView = UIView() super.init(frame: frame) self.addSubview(self.dimView) self.layer.addSublayer(self.backgroundLayer) self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true self.scrollView.clipsToBounds = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollView.contentInsetAdjustmentBehavior = .never } if #available(iOS 13.0, *) { self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false } self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceHorizontal = false self.scrollView.alwaysBounceVertical = true self.scrollView.scrollsToTop = false self.scrollView.delegate = self self.scrollView.clipsToBounds = true self.addSubview(self.scrollContentClippingView) self.scrollContentClippingView.addSubview(self.scrollView) self.scrollView.addSubview(self.scrollContentView) self.addSubview(self.navigationBarContainer) self.navigationBarContainer.addSubview(self.navigationBackgroundView) self.navigationBarContainer.layer.addSublayer(self.navigationBarSeparator) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) } } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } if !self.backgroundLayer.frame.contains(point) { return self.dimView } if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { return result } let result = super.hitTest(point, with: event) return result } @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { guard let environment = self.environment, let controller = environment.controller() else { return } controller.dismiss() } } private func calculateResult() -> RecentActionsSettingsSheet.Value { var events: AdminLogEventsFlags = [] var admins: [EnginePeer.Id] = [] for action in self.selectedMembersActions { events.formUnion(action.eventFlags) } for action in self.selectedSettingsActions { events.formUnion(action.eventFlags) } for action in self.selectedMessagesActions { events.formUnion(action.eventFlags) } for peerId in self.selectedAdmins { admins.append(peerId) } return RecentActionsSettingsSheet.Value( events: events, admins: admins ) } private func updateScrolling(transition: Transition) { guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { return } var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset let navigationAlpha: CGFloat = 1.0 - max(0.0, min(1.0, (topOffset + 20.0) / 20.0)) transition.setAlpha(view: self.navigationBackgroundView, alpha: navigationAlpha) transition.setAlpha(layer: self.navigationBarSeparator, alpha: navigationAlpha) topOffset = max(0.0, topOffset) transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) self.topOffsetDistance = topOffsetDistance var topOffsetFraction = topOffset / topOffsetDistance topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) let transitionFactor: CGFloat = 1.0 - topOffsetFraction if self.isUpdating { DispatchQueue.main.async { [weak controller] in guard let controller else { return } controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) } } else { controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) } } func animateIn() { self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) if let actionButtonView = self.actionButton.view { actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } } func animateOut(completion: @escaping () -> Void) { let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in completion() }) self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) if let actionButtonView = self.actionButton.view { actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) } if let environment = self.environment, let controller = environment.controller() { controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) } } func update(component: RecentActionsSettingsSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme let resetScrolling = self.scrollView.bounds.width != availableSize.width let sideInset: CGFloat = 16.0 + environment.safeInsets.left if self.component == nil { self.selectedMembersActions = Set(MembersActionType.actionTypesFromFlags(component.initialValue.events)) self.selectedSettingsActions = Set(SettingsActionType.actionTypesFromFlags(component.initialValue.events)) self.selectedMessagesActions = Set(MessagesActionType.actionTypesFromFlags(component.initialValue.events)) self.selectedAdmins = component.initialValue.admins.flatMap { Set($0) } ?? Set(component.adminPeers.map(\.id)) } var isGroup = true if case let .channel(channel) = component.peer, case .broadcast = channel.info { isGroup = false } self.component = component self.state = state self.environment = environment if themeUpdated { self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor } let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) var contentHeight: CGFloat = 0.0 contentHeight += 54.0 contentHeight += 16.0 let leftButtonSize = self.leftButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)), action: { [weak self] in guard let self, let controller = self.environment?.controller() else { return } controller.dismiss() } ).minSize(CGSize(width: 44.0, height: 56.0))), environment: {}, containerSize: CGSize(width: 120.0, height: 100.0) ) let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: 0.0), size: leftButtonSize) if let leftButtonView = self.leftButton.view { if leftButtonView.superview == nil { self.navigationBarContainer.addSubview(leftButtonView) } transition.setFrame(view: leftButtonView, frame: leftButtonFrame) } let containerInset: CGFloat = environment.statusBarHeight + 10.0 let clippingY: CGFloat let actionTypeSectionItem: (ActionTypeSection) -> AnyComponentWithIdentity = { actionTypeSection in let sectionId: AnyHashable let totalCount: Int let selectedCount: Int let isExpanded: Bool let title: String sectionId = actionTypeSection isExpanded = self.expandedSections.contains(actionTypeSection) switch actionTypeSection { case .members: totalCount = MembersActionType.allCases.count selectedCount = self.selectedMembersActions.count title = isGroup ? environment.strings.Channel_AdminLogFilter_Section_MembersGroup : environment.strings.Channel_AdminLogFilter_Section_MembersChannel case .settings: totalCount = SettingsActionType.allCases.count selectedCount = self.selectedSettingsActions.count title = isGroup ? environment.strings.Channel_AdminLogFilter_Section_SettingsGroup : environment.strings.Channel_AdminLogFilter_Section_SettingsChannel case .messages: totalCount = MessagesActionType.allCases.count selectedCount = self.selectedMessagesActions.count title = environment.strings.Channel_AdminLogFilter_Section_Messages } let itemTitle: AnyComponent = AnyComponent(HStack([ AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: title, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent( theme: environment.theme, title: "\(selectedCount)/\(totalCount)", isExpanded: isExpanded ))) ], spacing: 7.0)) let toggleAction: () -> Void = { [weak self] in guard let self else { return } switch actionTypeSection { case .members: if self.selectedMembersActions.isEmpty { self.selectedMembersActions = Set(MembersActionType.allCases) } else { self.selectedMembersActions.removeAll() } case .settings: if self.selectedSettingsActions.isEmpty { self.selectedSettingsActions = Set(SettingsActionType.allCases) } else { self.selectedSettingsActions.removeAll() } case .messages: if self.selectedMessagesActions.isEmpty { self.selectedMessagesActions = Set(MessagesActionType.allCases) } else { self.selectedMessagesActions.removeAll() } } self.state?.updated(transition: .spring(duration: 0.35)) } return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: itemTitle, leftIcon: .check(ListActionItemComponent.LeftIcon.Check( isSelected: selectedCount == totalCount, toggle: { toggleAction() } )), icon: .none, accessory: nil, action: { [weak self] _ in guard let self else { return } if self.expandedSections.contains(actionTypeSection) { self.expandedSections.remove(actionTypeSection) } else { self.expandedSections.insert(actionTypeSection) } self.state?.updated(transition: .spring(duration: 0.35)) }, highlighting: .disabled ))) } let expandedActionTypeSectionItem: (ActionTypeSection) -> AnyComponentWithIdentity = { actionTypeSection in let sectionId: AnyHashable let selectedActionTypes: Set let actionTypes: [ActionType] switch actionTypeSection { case .members: sectionId = "members-sub" actionTypes = MembersActionType.allCases.map(ActionType.members) selectedActionTypes = Set(self.selectedMembersActions.map(ActionType.members)) case .settings: sectionId = "settings-sub" actionTypes = SettingsActionType.allCases.map(ActionType.settings) selectedActionTypes = Set(self.selectedSettingsActions.map(ActionType.settings)) case .messages: sectionId = "messages-sub" actionTypes = MessagesActionType.allCases.map(ActionType.messages) selectedActionTypes = Set(self.selectedMessagesActions.map(ActionType.messages)) } var subItems: [AnyComponentWithIdentity] = [] for actionType in actionTypes { let actionItemTitle: String = actionType.title(isGroup: isGroup, strings: environment.strings) let subItemToggleAction: () -> Void = { [weak self] in guard let self else { return } switch actionType { case let .members(value): if self.selectedMembersActions.contains(value) { self.selectedMembersActions.remove(value) } else { self.selectedMembersActions.insert(value) } case let .settings(value): if self.selectedSettingsActions.contains(value) { self.selectedSettingsActions.remove(value) } else { self.selectedSettingsActions.insert(value) } case let .messages(value): if self.selectedMessagesActions.contains(value) { self.selectedMessagesActions.remove(value) } else { self.selectedMessagesActions.insert(value) } } self.state?.updated(transition: .spring(duration: 0.35)) } subItems.append(AnyComponentWithIdentity(id: actionType, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: actionItemTitle, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), leftIcon: .check(ListActionItemComponent.LeftIcon.Check( isSelected: selectedActionTypes.contains(actionType), toggle: { subItemToggleAction() } )), icon: .none, accessory: .none, action: { _ in subItemToggleAction() }, highlighting: .disabled )))) } return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListSubSectionComponent( theme: environment.theme, leftInset: 62.0, items: subItems ))) } let titleString: String = environment.strings.Channel_AdminLogFilter_RecentActionsTitle let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((54.0 - titleSize.height) * 0.5)), size: titleSize) if let titleView = title.view { if titleView.superview == nil { self.navigationBarContainer.addSubview(titleView) } transition.setFrame(view: titleView, frame: titleFrame) } let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0)) transition.setFrame(view: self.navigationBackgroundView, frame: navigationBackgroundFrame) self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) var optionsSectionItems: [AnyComponentWithIdentity] = [] for actionTypeSection in ActionTypeSection.allCases { optionsSectionItems.append(actionTypeSectionItem(actionTypeSection)) if self.expandedSections.contains(actionTypeSection) { optionsSectionItems.append(expandedActionTypeSectionItem(actionTypeSection)) } } let optionsSectionTransition = transition let optionsSectionSize = self.optionsSection.update( transition: optionsSectionTransition, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.Channel_AdminLogFilter_FilterActionsTypeTitle, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), footer: nil, items: optionsSectionItems )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) ) let optionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: optionsSectionSize) if let optionsSectionView = self.optionsSection.view { if optionsSectionView.superview == nil { self.scrollContentView.addSubview(optionsSectionView) self.optionsSection.parentState = state } transition.setFrame(view: optionsSectionView, frame: optionsSectionFrame) } contentHeight += optionsSectionSize.height contentHeight += 24.0 var peerItems: [AnyComponentWithIdentity] = [] for peer in component.adminPeers { peerItems.append(AnyComponentWithIdentity(id: peer.id, component: AnyComponent(AdminUserActionsPeerComponent( context: component.context, theme: environment.theme, strings: environment.strings, sideInset: 0.0, title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), peer: peer, selectionState: .editing(isSelected: self.selectedAdmins.contains(peer.id)), action: { [weak self] peer in guard let self else { return } if self.selectedAdmins.contains(peer.id) { self.selectedAdmins.remove(peer.id) } else { self.selectedAdmins.insert(peer.id) } self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .easeInOut))) } )))) } var adminsSectionItems: [AnyComponentWithIdentity] = [] let allAdminsToggleAction: () -> Void = { [weak self] in guard let self, let component = self.component else { return } if self.selectedAdmins.isEmpty { self.selectedAdmins = Set(component.adminPeers.map(\.id)) } else { self.selectedAdmins.removeAll() } self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .easeInOut))) } adminsSectionItems.append(AnyComponentWithIdentity(id: adminsSectionItems.count, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.Channel_AdminLogFilter_ShowAllAdminsActions, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), leftIcon: .check(ListActionItemComponent.LeftIcon.Check( isSelected: self.selectedAdmins.count == component.adminPeers.count, toggle: { allAdminsToggleAction() } )), icon: .none, accessory: .none, action: { _ in allAdminsToggleAction() }, highlighting: .disabled )))) adminsSectionItems.append(AnyComponentWithIdentity(id: adminsSectionItems.count, component: AnyComponent(ListSubSectionComponent( theme: environment.theme, leftInset: 62.0, items: peerItems )))) let adminsSectionSize = self.adminsSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.Channel_AdminLogFilter_FilterActionsAdminsTitle, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), footer: nil, items: adminsSectionItems )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) ) let adminsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: adminsSectionSize) if let adminsSectionView = self.adminsSection.view { if adminsSectionView.superview == nil { self.scrollContentView.addSubview(adminsSectionView) self.adminsSection.parentState = state } transition.setFrame(view: adminsSectionView, frame: adminsSectionFrame) } contentHeight += adminsSectionSize.height contentHeight += 30.0 let actionButtonSize = self.actionButton.update( transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: AnyComponentWithIdentity( id: AnyHashable(0), component: AnyComponent(ButtonTextContentComponent( text: environment.strings.Channel_AdminLogFilter_ApplyFilter, badge: 0, textColor: environment.theme.list.itemCheckColors.foregroundColor, badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, badgeForeground: environment.theme.list.itemCheckColors.fillColor )) ), isEnabled: true, displaysProgress: false, action: { [weak self] in guard let self, let component = self.component else { return } self.environment?.controller()?.dismiss() component.completion(self.calculateResult()) } )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) ) let bottomPanelHeight = 8.0 + environment.safeInsets.bottom + actionButtonSize.height let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) if let actionButtonView = actionButton.view { if actionButtonView.superview == nil { self.addSubview(actionButtonView) } transition.setFrame(view: actionButtonView, frame: actionButtonFrame) } contentHeight += bottomPanelHeight clippingY = actionButtonFrame.minY - 24.0 let topInset: CGFloat = max(0.0, availableSize.height - containerInset - contentHeight) let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) self.scrollContentClippingView.layer.cornerRadius = 10.0 self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset)) transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) self.ignoreScrolling = true let previousBounds = self.scrollView.bounds transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) if contentSize != self.scrollView.contentSize { self.scrollView.contentSize = contentSize } if resetScrolling { self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) } else { if !previousBounds.isEmpty, !transition.animation.isImmediate { let bounds = self.scrollView.bounds if bounds.maxY != previousBounds.maxY { let offsetY = previousBounds.maxY - bounds.maxY transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) } } } self.ignoreScrolling = false self.updateScrolling(transition: transition) return availableSize } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } public class RecentActionsSettingsSheet: ViewControllerComponentContainer { public final class Value { public let events: AdminLogEventsFlags public let admins: [EnginePeer.Id]? public init(events: AdminLogEventsFlags, admins: [EnginePeer.Id]?) { self.events = events self.admins = admins } } private let context: AccountContext private var isDismissed: Bool = false public init(context: AccountContext, peer: EnginePeer, adminPeers: [EnginePeer], initialValue: Value, completion: @escaping (Value) -> Void) { self.context = context super.init(context: context, component: RecentActionsSettingsSheetComponent(context: context, peer: peer, adminPeers: adminPeers, initialValue: initialValue, completion: completion), navigationBarAppearance: .none) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal self.blocksBackgroundWhenInOverlay = true } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.view.disablesInteractiveModalDismiss = true if let componentView = self.node.hostView.componentView as? RecentActionsSettingsSheetComponent.View { componentView.animateIn() } } override public func dismiss(completion: (() -> Void)? = nil) { if !self.isDismissed { self.isDismissed = true if let componentView = self.node.hostView.componentView as? RecentActionsSettingsSheetComponent.View { componentView.animateOut(completion: { [weak self] in completion?() self?.dismiss(animated: false) }) } else { self.dismiss(animated: false) } } } } private final class MediaSectionExpandIndicatorComponent: Component { let theme: PresentationTheme let title: String let isExpanded: Bool init( theme: PresentationTheme, title: String, isExpanded: Bool ) { self.theme = theme self.title = title self.isExpanded = isExpanded } static func ==(lhs: MediaSectionExpandIndicatorComponent, rhs: MediaSectionExpandIndicatorComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.title != rhs.title { return false } if lhs.isExpanded != rhs.isExpanded { return false } return true } final class View: UIView { private let arrowView: UIImageView private let title = ComponentView() override init(frame: CGRect) { self.arrowView = UIImageView() super.init(frame: frame) self.addSubview(self.arrowView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: MediaSectionExpandIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let titleArrowSpacing: CGFloat = 1.0 if self.arrowView.image == nil { self.arrowView.image = PresentationResourcesItemList.expandDownArrowImage(component.theme) } self.arrowView.tintColor = component.theme.list.itemPrimaryTextColor let arrowSize = self.arrowView.image?.size ?? CGSize(width: 1.0, height: 1.0) let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.title, font: Font.semibold(13.0), textColor: component.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) let size = CGSize(width: titleSize.width + titleArrowSpacing + arrowSize.width, height: titleSize.height) let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) let arrowFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + titleArrowSpacing, y: floor((size.height - arrowSize.height) * 0.5) + 2.0), size: arrowSize) if let titleView = self.title.view { if titleView.superview == nil { self.addSubview(titleView) } titleView.frame = titleFrame } self.arrowView.center = arrowFrame.center self.arrowView.bounds = CGRect(origin: CGPoint(), size: arrowFrame.size) transition.setTransform(view: self.arrowView, transform: CATransform3DTranslate(CATransform3DMakeRotation(component.isExpanded ? CGFloat.pi : 0.0, 0.0, 0.0, 1.0), 0.0, component.isExpanded ? 1.0 : -1.0, 0.0)) return size } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }