diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift new file mode 100644 index 0000000000..3a603b2143 --- /dev/null +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift @@ -0,0 +1,990 @@ +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(strings: PresentationStrings) -> String { + switch self { + case .newAdminRights: + return "New Admin Rights" + case .newExceptions: + return "New Exceptions" + case .newMembers: + return "New Members" + case .leftMembers: + return "Members left the Group" + } + } +} + +private enum SettingsActionType: Hashable, CaseIterable { + case groupInfo + case inviteLinks + case videoChats + + func title(strings: PresentationStrings) -> String { + switch self { + case .groupInfo: + return "Group Info" + case .inviteLinks: + return "Invite Links" + case .videoChats: + return "Video Chats" + } + } +} + +private enum MessagesActionType: Hashable, CaseIterable { + case deletedMessages + case editedMessages + case pinnedMessages + + func title(strings: PresentationStrings) -> String { + switch self { + case .deletedMessages: + return "Deleted Messages" + case .editedMessages: + return "Edited Messages" + case .pinnedMessages: + return "Pinned Messages" + } + } +} + +private enum ActionType: Hashable { + case members(MembersActionType) + case settings(SettingsActionType) + case messages(MessagesActionType) + + func title(strings: PresentationStrings) -> String { + switch self { + case let .members(value): + return value.title(strings: strings) + case let .settings(value): + return value.title(strings: strings) + case let .messages(value): + return value.title(strings: strings) + } + } +} + +private final class RecentActionsSettingsSheetComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let adminPeers: [EnginePeer] + let completion: (RecentActionsSettingsSheet.Result) -> Void + + init( + context: AccountContext, + adminPeers: [EnginePeer], + completion: @escaping (RecentActionsSettingsSheet.Result) -> Void + ) { + self.context = context + self.adminPeers = adminPeers + self.completion = completion + } + + static func ==(lhs: RecentActionsSettingsSheetComponent, rhs: RecentActionsSettingsSheetComponent) -> Bool { + if lhs.context !== rhs.context { + 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.Result { + return RecentActionsSettingsSheet.Result( + ) + } + + 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 + + if self.component == nil { + self.selectedMembersActions = Set(MembersActionType.allCases) + self.selectedSettingsActions = Set(SettingsActionType.allCases) + self.selectedMessagesActions = Set(MessagesActionType.allCases) + self.selectedAdmins = Set(component.adminPeers.map(\.id)) + } + + 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, 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 = "Members and Admins" + case .settings: + totalCount = SettingsActionType.allCases.count + selectedCount = self.selectedSettingsActions.count + title = "Group Settings" + case .messages: + totalCount = MessagesActionType.allCases.count + selectedCount = self.selectedMessagesActions.count + title = "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 != 0, + 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(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 + ))) + } + + //TODO:localize + let titleString: String = "Recent Actions" + 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: "FILTER ACTIONS BY TYPE", + 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: "Show Actions by All Admins", + 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.isEmpty, + 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: "FILTER ACTIONS BY ADMINS", + 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: "Apply Filter", + 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 Result { + init() { + } + } + + private let context: AccountContext + + private var isDismissed: Bool = false + + public init(context: AccountContext, adminPeers: [EnginePeer], completion: @escaping (Result) -> Void) { + self.context = context + + super.init(context: context, component: RecentActionsSettingsSheetComponent(context: context, adminPeers: adminPeers, 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) + } +} diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index ebb2d8942d..c07cb0e544 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -131,6 +131,11 @@ extension ChatControllerImpl { return } + /*if "".isEmpty { + self.push(RecentActionsSettingsSheet(context: self.context, adminPeers: authors.map(EnginePeer.init), completion: { _ in })) + return + }*/ + self.navigationActionDisposable.set((combineLatest(authors.map { author in self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id) })