2024-04-19 11:15:18 +04:00

1091 lines
50 KiB
Swift

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"
}
}
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(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<Empty>()
private let title = ComponentView<Empty>()
private let actionButton = ComponentView<Empty>()
private let optionsSection = ComponentView<Empty>()
private let adminsSection = ComponentView<Empty>()
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<ActionTypeSection>()
private var selectedMembersActions = Set<MembersActionType>()
private var selectedSettingsActions = Set<SettingsActionType>()
private var selectedMessagesActions = Set<MessagesActionType>()
private var selectedAdmins = Set<EnginePeer.Id>()
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<CGPoint>) {
}
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<ViewControllerComponentContainer.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<Empty> = { 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 ? "Members and Admins" : "Subscribers and Admins"
case .settings:
totalCount = SettingsActionType.allCases.count
selectedCount = self.selectedSettingsActions.count
title = isGroup ? "Group Settings" : "Channel Settings"
case .messages:
totalCount = MessagesActionType.allCases.count
selectedCount = self.selectedMessagesActions.count
title = "Messages"
}
let itemTitle: AnyComponent<Empty> = 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<Empty> = { actionTypeSection in
let sectionId: AnyHashable
let selectedActionTypes: Set<ActionType>
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<Empty>] = []
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
)))
}
//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<Empty>] = []
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<Empty>] = []
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<Empty>] = []
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<ViewControllerComponentContainer.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<Empty>()
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<Empty>, 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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}