Telegram-iOS/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift
2026-04-22 23:03:51 +02:00

1879 lines
86 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import ButtonComponent
import PresentationDataUtils
import ListSectionComponent
import ListActionItemComponent
import PlainButtonComponent
import ResizableSheetComponent
import GlassBarButtonComponent
import BundleIconComponent
struct MediaRight: OptionSet, Hashable {
var rawValue: Int
static let photos = MediaRight(rawValue: 1 << 0)
static let videos = MediaRight(rawValue: 1 << 1)
static let stickersAndGifs = MediaRight(rawValue: 1 << 2)
static let music = MediaRight(rawValue: 1 << 3)
static let files = MediaRight(rawValue: 1 << 4)
static let voiceMessages = MediaRight(rawValue: 1 << 5)
static let videoMessages = MediaRight(rawValue: 1 << 6)
static let links = MediaRight(rawValue: 1 << 7)
static let polls = MediaRight(rawValue: 1 << 8)
}
extension MediaRight {
var count: Int {
var result = 0
var index = 0
while index < 31 {
let currentValue = self.rawValue >> UInt32(index)
index += 1
if currentValue == 0 {
break
}
if (currentValue & 1) != 0 {
result += 1
}
}
return result
}
}
private struct ParticipantRight: OptionSet {
var rawValue: Int
static let sendMessages = ParticipantRight(rawValue: 1 << 0)
static let addMembers = ParticipantRight(rawValue: 1 << 2)
static let pinMessages = ParticipantRight(rawValue: 1 << 3)
static let changeInfo = ParticipantRight(rawValue: 1 << 4)
}
private func rightsFromBannedRights(_ rights: TelegramChatBannedRightsFlags) -> (participantRights: ParticipantRight, mediaRights: MediaRight) {
var participantResult: ParticipantRight = [
.sendMessages,
.addMembers,
.pinMessages,
.changeInfo
]
var mediaResult: MediaRight = [
.photos,
.videos,
.stickersAndGifs,
.music,
.files,
.voiceMessages,
.videoMessages,
.links,
.polls
]
if rights.contains(.banSendText) {
participantResult.remove(.sendMessages)
}
if rights.contains(.banAddMembers) {
participantResult.remove(.addMembers)
}
if rights.contains(.banPinMessages) {
participantResult.remove(.pinMessages)
}
if rights.contains(.banChangeInfo) {
participantResult.remove(.changeInfo)
}
if rights.contains(.banSendPhotos) {
mediaResult.remove(.photos)
}
if rights.contains(.banSendVideos) {
mediaResult.remove(.videos)
}
if rights.contains(.banSendStickers) || rights.contains(.banSendGifs) || rights.contains(.banSendGames) || rights.contains(.banSendInline) {
mediaResult.remove(.stickersAndGifs)
}
if rights.contains(.banSendMusic) {
mediaResult.remove(.music)
}
if rights.contains(.banSendFiles) {
mediaResult.remove(.files)
}
if rights.contains(.banSendVoice) {
mediaResult.remove(.voiceMessages)
}
if rights.contains(.banSendInstantVideos) {
mediaResult.remove(.videoMessages)
}
if rights.contains(.banEmbedLinks) {
mediaResult.remove(.links)
}
if rights.contains(.banSendPolls) {
mediaResult.remove(.polls)
}
return (participantResult, mediaResult)
}
private func rightFlagsFromRights(participantRights: ParticipantRight, mediaRights: MediaRight) -> TelegramChatBannedRightsFlags {
var result: TelegramChatBannedRightsFlags = []
if !participantRights.contains(.sendMessages) {
result.insert(.banSendText)
}
if !participantRights.contains(.addMembers) {
result.insert(.banAddMembers)
}
if !participantRights.contains(.pinMessages) {
result.insert(.banPinMessages)
}
if !participantRights.contains(.changeInfo) {
result.insert(.banChangeInfo)
}
if !mediaRights.contains(.photos) {
result.insert(.banSendPhotos)
}
if !mediaRights.contains(.videos) {
result.insert(.banSendVideos)
}
if !mediaRights.contains(.stickersAndGifs) {
result.insert(.banSendStickers)
result.insert(.banSendGifs)
result.insert(.banSendGames)
result.insert(.banSendInline)
}
if !mediaRights.contains(.music) {
result.insert(.banSendMusic)
}
if !mediaRights.contains(.files) {
result.insert(.banSendFiles)
}
if !mediaRights.contains(.voiceMessages) {
result.insert(.banSendVoice)
}
if !mediaRights.contains(.videoMessages) {
result.insert(.banSendInstantVideos)
}
if !mediaRights.contains(.links) {
result.insert(.banEmbedLinks)
}
if !mediaRights.contains(.polls) {
result.insert(.banSendPolls)
}
return result
}
private let allMediaRightItems: [MediaRight] = [
.photos,
.videos,
.stickersAndGifs,
.music,
.files,
.voiceMessages,
.videoMessages,
.links,
.polls
]
private enum AdminUserActionOptionSection {
case report
case deleteAll
case ban
}
private enum AdminUserActionConfigItem: Hashable, CaseIterable {
case sendMessages
case sendMedia
case addUsers
case pinMessages
case changeInfo
}
private struct AdminUserActionsSheetState: Equatable {
var isOptionReportExpanded: Bool
var optionReportSelectedPeers: Set<EnginePeer.Id>
var isOptionDeleteAllExpanded: Bool
var optionDeleteAllSelectedPeers: Set<EnginePeer.Id>
var optionDeleteAllReactionsSelectedPeers: Set<EnginePeer.Id>
var isOptionBanExpanded: Bool
var optionBanSelectedPeers: Set<EnginePeer.Id>
var isConfigurationExpanded: Bool
var isMediaSectionExpanded: Bool
var allowedParticipantRights: ParticipantRight
var allowedMediaRights: MediaRight
var participantRights: ParticipantRight
var mediaRights: MediaRight
}
private func availableAdminUserActionOptionSections(
accountPeerId: EnginePeer.Id,
chatPeer: EnginePeer,
peers: [RenderedChannelParticipant],
mode: AdminUserActionsSheet.Mode
) -> [AdminUserActionOptionSection] {
var result: [AdminUserActionOptionSection] = [.report]
switch mode {
case .monoforum:
result.append(.ban)
case .chat, .chatReaction:
if case let .channel(channel) = chatPeer {
if channel.hasPermission(.deleteAllMessages) {
result.append(.deleteAll)
if channel.hasPermission(.banMembers) {
var canBanEveryone = true
for peer in peers {
if peer.peer.id == accountPeerId {
canBanEveryone = false
continue
}
switch peer.participant {
case .creator:
canBanEveryone = false
case let .member(_, _, adminInfo, _, _, _):
if let adminInfo {
if channel.flags.contains(.isCreator) {
} else if adminInfo.promotedBy == accountPeerId {
} else {
canBanEveryone = false
}
}
}
}
if canBanEveryone {
result.append(.ban)
}
}
}
}
case .liveStream:
result.append(.deleteAll)
result.append(.ban)
}
return result
}
private func adminUserActionsTitle(
strings: PresentationStrings,
mode: AdminUserActionsSheet.Mode,
peers: [RenderedChannelParticipant],
selectedDeleteAllPeers: Set<EnginePeer.Id>
) -> String {
switch mode {
case .monoforum:
if let peer = peers.first {
return strings.Monoforum_DeleteTopic_Title(EnginePeer(peer.peer).compactDisplayTitle).string
} else {
return strings.Common_Delete
}
case let .chat(messageCount, deleteAllMessageCount, _):
if let deleteAllMessageCount, selectedDeleteAllPeers == Set(peers.map { $0.peer.id }) {
return strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount))
} else {
return strings.Chat_AdminActionSheet_DeleteTitle(Int32(messageCount))
}
case let .liveStream(messageCount, deleteAllMessageCount, _):
if let deleteAllMessageCount, selectedDeleteAllPeers == Set(peers.map { $0.peer.id }) {
return strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount))
} else {
return strings.Chat_AdminActionSheet_DeleteTitle(Int32(messageCount))
}
case .chatReaction(_):
//TODO:localize
return "Delete 1 Reaction"
}
}
private final class AdminUserActionsContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let chatPeer: EnginePeer
let peers: [RenderedChannelParticipant]
let mode: AdminUserActionsSheet.Mode
let theme: PresentationTheme
let strings: PresentationStrings
let presentationData: PresentationData
let sheetState: AdminUserActionsSheetState
let disableOptionsSectionAnimation: Bool
let toggleOptionSelection: (AdminUserActionOptionSection) -> Void
let toggleOptionExpansion: (AdminUserActionOptionSection) -> Void
let togglePeerSelection: (AdminUserActionOptionSection, EnginePeer) -> Void
let toggleConfiguration: () -> Void
let toggleConfigItem: (AdminUserActionConfigItem) -> Void
let toggleMediaSectionExpansion: () -> Void
let toggleMediaRight: (MediaRight) -> Void
init(
context: AccountContext,
chatPeer: EnginePeer,
peers: [RenderedChannelParticipant],
mode: AdminUserActionsSheet.Mode,
theme: PresentationTheme,
strings: PresentationStrings,
presentationData: PresentationData,
sheetState: AdminUserActionsSheetState,
disableOptionsSectionAnimation: Bool,
toggleOptionSelection: @escaping (AdminUserActionOptionSection) -> Void,
toggleOptionExpansion: @escaping (AdminUserActionOptionSection) -> Void,
togglePeerSelection: @escaping (AdminUserActionOptionSection, EnginePeer) -> Void,
toggleConfiguration: @escaping () -> Void,
toggleConfigItem: @escaping (AdminUserActionConfigItem) -> Void,
toggleMediaSectionExpansion: @escaping () -> Void,
toggleMediaRight: @escaping (MediaRight) -> Void
) {
self.context = context
self.chatPeer = chatPeer
self.peers = peers
self.mode = mode
self.theme = theme
self.strings = strings
self.presentationData = presentationData
self.sheetState = sheetState
self.disableOptionsSectionAnimation = disableOptionsSectionAnimation
self.toggleOptionSelection = toggleOptionSelection
self.toggleOptionExpansion = toggleOptionExpansion
self.togglePeerSelection = togglePeerSelection
self.toggleConfiguration = toggleConfiguration
self.toggleConfigItem = toggleConfigItem
self.toggleMediaSectionExpansion = toggleMediaSectionExpansion
self.toggleMediaRight = toggleMediaRight
}
static func ==(lhs: AdminUserActionsContentComponent, rhs: AdminUserActionsContentComponent) -> Bool {
return false
}
final class View: UIView {
private let optionsSection = ComponentView<Empty>()
private let optionsFooter = ComponentView<Empty>()
private let configSection = ComponentView<Empty>()
func update(component: AdminUserActionsContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let sideInset: CGFloat = 16.0
var contentHeight: CGFloat = 76.0
contentHeight += 15.0
let availableOptions = availableAdminUserActionOptionSections(
accountPeerId: component.context.account.peerId,
chatPeer: component.chatPeer,
peers: component.peers,
mode: component.mode
)
let optionsItem: (AdminUserActionOptionSection) -> AnyComponentWithIdentity<Empty> = { section in
let sectionId: AnyHashable
let selectedPeers: Set<EnginePeer.Id>
var additionalSelectedPeers = Set<EnginePeer.Id>()
let isExpanded: Bool
let title: String
switch section {
case .report:
sectionId = "report"
selectedPeers = component.sheetState.optionReportSelectedPeers
isExpanded = component.sheetState.isOptionReportExpanded
title = component.strings.Chat_AdminActionSheet_ReportSpam
case .deleteAll:
sectionId = "delete-all"
selectedPeers = component.sheetState.optionDeleteAllSelectedPeers
additionalSelectedPeers = component.sheetState.optionDeleteAllReactionsSelectedPeers
isExpanded = component.sheetState.isOptionDeleteAllExpanded
if component.peers.count == 1 {
title = component.strings.Chat_AdminActionSheet_DeleteAllSingle(EnginePeer(component.peers[0].peer).compactDisplayTitle).string
} else {
title = component.strings.Chat_AdminActionSheet_DeleteAllMultiple
}
case .ban:
sectionId = "ban"
selectedPeers = component.sheetState.optionBanSelectedPeers
isExpanded = component.sheetState.isOptionBanExpanded
let banTitle: String
let restrictTitle: String
if component.peers.count == 1 {
banTitle = component.strings.Chat_AdminActionSheet_BanSingle(EnginePeer(component.peers[0].peer).compactDisplayTitle).string
restrictTitle = component.strings.Chat_AdminActionSheet_RestrictSingle(EnginePeer(component.peers[0].peer).compactDisplayTitle).string
} else {
banTitle = component.strings.Chat_AdminActionSheet_BanMultiple
restrictTitle = component.strings.Chat_AdminActionSheet_RestrictMultiple
}
title = component.sheetState.isConfigurationExpanded ? restrictTitle : banTitle
}
var titleItems: [AnyComponentWithIdentity<Empty>] = [
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: title,
font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize),
textColor: component.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)))
]
var accessory: ListActionItemComponent.Accessory?
var isExpandable = false
if component.peers.count > 1 {
accessory = .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(OptionSectionExpandIndicatorComponent(
theme: component.theme,
count: selectedPeers.isEmpty ? component.peers.count : selectedPeers.count,
isExpanded: isExpanded
)),
effectAlignment: .center,
action: {
component.toggleOptionExpansion(section)
},
animateScale: false
))),
insets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 2.0),
isInteractive: true
))
} else if case .deleteAll = section {
var count = 0
if !selectedPeers.isEmpty {
count += 1
}
if !additionalSelectedPeers.isEmpty {
count += 1
}
titleItems.append(
AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent(
theme: component.theme,
title: "\(count)/2",
isExpanded: component.sheetState.isMediaSectionExpanded
)))
)
isExpandable = true
}
return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListActionItemComponent(
theme: component.theme,
style: .glass,
title: AnyComponent(HStack(titleItems, spacing: 7.0)),
leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
isSelected: !selectedPeers.isEmpty || !additionalSelectedPeers.isEmpty,
toggle: {
component.toggleOptionSelection(section)
}
)),
icon: .none,
accessory: accessory,
action: { _ in
if isExpandable {
component.toggleOptionExpansion(section)
} else {
component.toggleOptionSelection(section)
}
},
highlighting: .disabled
)))
}
let expandedPeersItem: (AdminUserActionOptionSection) -> AnyComponentWithIdentity<Empty> = { section in
let sectionId: AnyHashable
let selectedPeers: Set<EnginePeer.Id>
var additionalSelectedPeers = Set<EnginePeer.Id>()
switch section {
case .report:
sectionId = "report-peers"
selectedPeers = component.sheetState.optionReportSelectedPeers
case .deleteAll:
sectionId = "delete-all-peers"
selectedPeers = component.sheetState.optionDeleteAllSelectedPeers
additionalSelectedPeers = component.sheetState.optionDeleteAllReactionsSelectedPeers
case .ban:
sectionId = "ban-peers"
selectedPeers = component.sheetState.optionBanSelectedPeers
}
var subItems: [AnyComponentWithIdentity<Empty>] = []
if component.peers.count > 1 {
for peer in component.peers {
subItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(AdminUserActionsPeerComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
baseFontSize: component.presentationData.listsFontSize.baseDisplaySize,
sideInset: 0.0,
title: EnginePeer(peer.peer).displayTitle(strings: component.strings, displayOrder: .firstLast),
peer: EnginePeer(peer.peer),
selectionState: .editing(isSelected: selectedPeers.contains(peer.peer.id)),
action: { peer in
component.togglePeerSelection(section, peer)
}
))))
}
} else {
//TODO:localize
subItems.append(
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: component.theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Delete all Messages",
font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize),
textColor: component.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
isSelected: !selectedPeers.isEmpty,
toggle: {
component.toggleOptionSelection(section)
}
)),
icon: .none,
accessory: nil,
action: { _ in
component.toggleOptionSelection(section)
},
highlighting: .disabled
)))
)
subItems.append(
AnyComponentWithIdentity(id: 1, component: AnyComponent(ListActionItemComponent(
theme: component.theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Delete all Reactions",
font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize),
textColor: component.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
isSelected: !additionalSelectedPeers.isEmpty,
toggle: {
component.toggleOptionSelection(section)
}
)),
icon: .none,
accessory: nil,
action: { _ in
component.toggleOptionSelection(section)
},
highlighting: .disabled
)))
)
}
return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListSubSectionComponent(
theme: component.theme,
leftInset: 43.0,
items: subItems
)))
}
var optionsSectionItems: [AnyComponentWithIdentity<Empty>] = []
for option in availableOptions {
let isExpanded: Bool
switch option {
case .report:
isExpanded = component.sheetState.isOptionReportExpanded
case .deleteAll:
isExpanded = component.sheetState.isOptionDeleteAllExpanded
case .ban:
isExpanded = component.sheetState.isOptionBanExpanded
}
optionsSectionItems.append(optionsItem(option))
if isExpanded {
optionsSectionItems.append(expandedPeersItem(option))
}
}
let optionsSectionTransition: ComponentTransition = component.disableOptionsSectionAnimation ? transition.withAnimation(.none) : transition
let optionsSectionSize = self.optionsSection.update(
transition: optionsSectionTransition,
component: AnyComponent(ListSectionComponent(
theme: component.theme,
style: .glass,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.strings.Chat_AdminActionSheet_RestrictSectionHeader,
font: Font.regular(component.presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: component.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: optionsSectionItems,
isModal: true
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0)
)
let optionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: optionsSectionSize)
self.optionsSection.parentState = state
if let optionsSectionView = self.optionsSection.view {
if optionsSectionView.superview == nil {
self.addSubview(optionsSectionView)
}
transition.setFrame(view: optionsSectionView, frame: optionsSectionFrame)
}
contentHeight += optionsSectionSize.height
let partiallyRestrictTitle: String
let fullyBanTitle: String
if component.peers.count == 1 {
partiallyRestrictTitle = component.strings.Chat_AdminActionSheet_RestrictFooterSingle
fullyBanTitle = component.strings.Chat_AdminActionSheet_BanFooterSingle
} else {
partiallyRestrictTitle = component.strings.Chat_AdminActionSheet_RestrictFooterMultiple
fullyBanTitle = component.strings.Chat_AdminActionSheet_BanFooterMultiple
}
let optionsFooterSize = self.optionsFooter.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(OptionsSectionFooterComponent(
theme: component.theme,
text: component.sheetState.isConfigurationExpanded ? fullyBanTitle : partiallyRestrictTitle,
fontSize: component.presentationData.listsFontSize.itemListBaseHeaderFontSize,
isExpanded: component.sheetState.isConfigurationExpanded
)),
effectAlignment: .left,
contentInsets: UIEdgeInsets(),
action: {
component.toggleConfiguration()
},
animateAlpha: true,
animateScale: false,
animateContents: true
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
var configSectionItems: [AnyComponentWithIdentity<Empty>] = []
if case let .channel(channel) = component.chatPeer, channel.isMonoForum {
} else if case .liveStream = component.mode {
} else {
var allConfigItems: [(AdminUserActionConfigItem, Bool)] = []
if !component.sheetState.allowedMediaRights.isEmpty || !component.sheetState.allowedParticipantRights.isEmpty {
for configItem in AdminUserActionConfigItem.allCases {
let isEnabled: Bool
switch configItem {
case .sendMessages:
isEnabled = component.sheetState.allowedParticipantRights.contains(.sendMessages)
case .sendMedia:
isEnabled = !component.sheetState.allowedMediaRights.isEmpty
case .addUsers:
isEnabled = component.sheetState.allowedParticipantRights.contains(.addMembers)
case .pinMessages:
isEnabled = component.sheetState.allowedParticipantRights.contains(.pinMessages)
case .changeInfo:
isEnabled = component.sheetState.allowedParticipantRights.contains(.changeInfo)
}
allConfigItems.append((configItem, isEnabled))
}
}
for (configItem, isEnabled) in allConfigItems {
let itemTitle: AnyComponent<Empty>
let itemValue: Bool
switch configItem {
case .sendMessages:
itemTitle = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.strings.Channel_BanUser_PermissionSendMessages,
font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize),
textColor: component.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))
itemValue = component.sheetState.participantRights.contains(.sendMessages)
case .sendMedia:
if isEnabled {
itemTitle = AnyComponent(HStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.strings.Channel_BanUser_PermissionSendMedia,
font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize),
textColor: component.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent(
theme: component.theme,
title: "\(component.sheetState.mediaRights.count)/\(component.sheetState.allowedMediaRights.count)",
isExpanded: component.sheetState.isMediaSectionExpanded
)))
], spacing: 7.0))
} else {
itemTitle = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.strings.Channel_BanUser_PermissionSendMedia,
font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize),
textColor: component.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))
}
itemValue = !component.sheetState.mediaRights.isEmpty
case .addUsers:
itemTitle = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.strings.Channel_BanUser_PermissionAddMembers,
font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize),
textColor: component.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))
itemValue = component.sheetState.participantRights.contains(.addMembers)
case .pinMessages:
itemTitle = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.strings.Channel_EditAdmin_PermissionPinMessages,
font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize),
textColor: component.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))
itemValue = component.sheetState.participantRights.contains(.pinMessages)
case .changeInfo:
itemTitle = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.strings.Channel_BanUser_PermissionChangeGroupInfo,
font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize),
textColor: component.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))
itemValue = component.sheetState.participantRights.contains(.changeInfo)
}
configSectionItems.append(AnyComponentWithIdentity(id: configItem, component: AnyComponent(ListActionItemComponent(
theme: component.theme,
style: .glass,
title: itemTitle,
accessory: .toggle(ListActionItemComponent.Toggle(
style: isEnabled ? .icons : .lock,
isOn: itemValue,
isInteractive: isEnabled,
action: isEnabled ? { _ in
component.toggleConfigItem(configItem)
} : nil
)),
action: ((isEnabled && configItem == .sendMedia) || !isEnabled) ? { _ in
if !isEnabled {
environment.controller()?.present(textAlertController(
context: component.context,
title: nil,
text: component.strings.GroupPermission_PermissionDisabledByDefault,
actions: [
TextAlertAction(type: .defaultAction, title: component.strings.Common_OK, action: {
})
]
), in: .window(.root))
} else {
component.toggleMediaSectionExpansion()
}
} : nil,
highlighting: .disabled
))))
if isEnabled, case .sendMedia = configItem, component.sheetState.isMediaSectionExpanded {
var mediaItems: [AnyComponentWithIdentity<Empty>] = []
mediaRightsLoop: for possibleMediaItem in allMediaRightItems {
if !component.sheetState.allowedMediaRights.contains(possibleMediaItem) {
continue
}
let mediaItemTitle: String
switch possibleMediaItem {
case .photos:
mediaItemTitle = component.strings.Channel_BanUser_PermissionSendPhoto
case .videos:
mediaItemTitle = component.strings.Channel_BanUser_PermissionSendVideo
case .stickersAndGifs:
mediaItemTitle = component.strings.Channel_BanUser_PermissionSendStickersAndGifs
case .music:
mediaItemTitle = component.strings.Channel_BanUser_PermissionSendMusic
case .files:
mediaItemTitle = component.strings.Channel_BanUser_PermissionSendFile
case .voiceMessages:
mediaItemTitle = component.strings.Channel_BanUser_PermissionSendVoiceMessage
case .videoMessages:
mediaItemTitle = component.strings.Channel_BanUser_PermissionSendVideoMessage
case .links:
mediaItemTitle = component.strings.Channel_BanUser_PermissionEmbedLinks
case .polls:
mediaItemTitle = component.strings.Channel_BanUser_PermissionSendPolls
default:
continue mediaRightsLoop
}
mediaItems.append(AnyComponentWithIdentity(id: possibleMediaItem, component: AnyComponent(ListActionItemComponent(
theme: component.theme,
style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: mediaItemTitle,
font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize),
textColor: component.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)))
], alignment: .left, spacing: 2.0)),
leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
isSelected: component.sheetState.mediaRights.contains(possibleMediaItem),
toggle: {
component.toggleMediaRight(possibleMediaItem)
}
)),
icon: .none,
accessory: .none,
action: { _ in
component.toggleMediaRight(possibleMediaItem)
},
highlighting: .disabled
))))
}
configSectionItems.append(AnyComponentWithIdentity(id: "media-sub", component: AnyComponent(ListSubSectionComponent(
theme: component.theme,
leftInset: 0.0,
items: mediaItems
))))
}
}
}
let configSectionSize = self.configSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: component.theme,
style: .glass,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.peers.count == 1 ? component.strings.Chat_AdminActionSheet_PermissionsSectionHeader : component.strings.Chat_AdminActionSheet_PermissionsSectionHeaderMultiple,
font: Font.regular(component.presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: component.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: configSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0)
)
let configSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 30.0), size: configSectionSize)
self.configSection.parentState = state
if let configSectionView = self.configSection.view {
if configSectionView.superview == nil {
configSectionView.clipsToBounds = true
configSectionView.layer.cornerRadius = 11.0
self.addSubview(configSectionView)
}
let effectiveConfigSectionFrame: CGRect
if component.sheetState.isConfigurationExpanded {
effectiveConfigSectionFrame = configSectionFrame
} else {
effectiveConfigSectionFrame = CGRect(origin: CGPoint(x: configSectionFrame.minX, y: configSectionFrame.minY - 30.0), size: CGSize(width: configSectionFrame.width, height: 0.0))
}
transition.setFrame(view: configSectionView, frame: effectiveConfigSectionFrame)
transition.setAlpha(view: configSectionView, alpha: component.sheetState.isConfigurationExpanded ? 1.0 : 0.0)
}
if availableOptions.contains(.ban) && !configSectionItems.isEmpty {
let optionsFooterFrame: CGRect
if component.sheetState.isConfigurationExpanded {
contentHeight += 30.0
contentHeight += configSectionSize.height
contentHeight += 7.0
optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize)
contentHeight += optionsFooterSize.height
} else {
contentHeight += 7.0
optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize)
contentHeight += optionsFooterSize.height
}
self.optionsFooter.parentState = state
if let optionsFooterView = self.optionsFooter.view {
if optionsFooterView.superview == nil {
self.addSubview(optionsFooterView)
}
transition.setFrame(view: optionsFooterView, frame: optionsFooterFrame)
transition.setAlpha(view: optionsFooterView, alpha: 1.0)
}
} else {
self.optionsFooter.parentState = state
if let optionsFooterView = self.optionsFooter.view {
if optionsFooterView.superview == nil {
self.addSubview(optionsFooterView)
}
let optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize)
transition.setFrame(view: optionsFooterView, frame: optionsFooterFrame)
transition.setAlpha(view: optionsFooterView, alpha: 0.0)
}
}
contentHeight += 36.0 + 52.0 + 30.0
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class AdminUserActionsSheetComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let chatPeer: EnginePeer
let peers: [RenderedChannelParticipant]
let mode: AdminUserActionsSheet.Mode
init(
context: AccountContext,
chatPeer: EnginePeer,
peers: [RenderedChannelParticipant],
mode: AdminUserActionsSheet.Mode
) {
self.context = context
self.chatPeer = chatPeer
self.peers = peers
self.mode = mode
}
static func ==(lhs: AdminUserActionsSheetComponent, rhs: AdminUserActionsSheetComponent) -> Bool {
return true
}
final class View: UIView {
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>()
private let animateOut = ActionSlot<Action<Void>>()
private var component: AdminUserActionsSheetComponent?
private weak var state: EmptyComponentState?
private var environment: ViewControllerComponentContainer.Environment?
private var isUpdating: Bool = false
private var isDismissing: Bool = false
private var isOptionReportExpanded: Bool = false
private var optionReportSelectedPeers = Set<EnginePeer.Id>()
private var isOptionDeleteAllExpanded: Bool = false
private var optionDeleteAllSelectedPeers = Set<EnginePeer.Id>()
private var isOptionBanExpanded: Bool = false
private var optionBanSelectedPeers = Set<EnginePeer.Id>()
private var isConfigurationExpanded: Bool = false
private var isMediaSectionExpanded: Bool = false
private var allowedParticipantRights: ParticipantRight = []
private var allowedMediaRights: MediaRight = []
private var participantRights: ParticipantRight = []
private var mediaRights: MediaRight = []
private var previousWasConfigurationExpanded: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func calculateMonoforumResult() -> AdminUserActionsSheet.MonoforumResult {
return AdminUserActionsSheet.MonoforumResult(
ban: !self.optionBanSelectedPeers.isEmpty,
reportSpam: !self.optionReportSelectedPeers.isEmpty
)
}
private func calculateChatResult() -> AdminUserActionsSheet.ChatResult {
var reportSpamPeers: [EnginePeer.Id] = []
var deleteAllFromPeers: [EnginePeer.Id] = []
var deleteAllReactionsFromPeers: [EnginePeer.Id] = []
var banPeers: [EnginePeer.Id] = []
var updateBannedRights: [EnginePeer.Id: TelegramChatBannedRights] = [:]
for id in self.optionReportSelectedPeers.sorted() {
reportSpamPeers.append(id)
}
for id in self.optionDeleteAllSelectedPeers.sorted() {
deleteAllFromPeers.append(id)
}
for id in self.optionDeleteAllSelectedPeers.sorted() {
deleteAllReactionsFromPeers.append(id)
}
if !self.isConfigurationExpanded {
for id in self.optionBanSelectedPeers.sorted() {
banPeers.append(id)
}
} else {
let banFlags = rightFlagsFromRights(participantRights: self.participantRights, mediaRights: self.mediaRights)
let bannedRights = TelegramChatBannedRights(flags: banFlags, untilDate: Int32.max)
for id in self.optionBanSelectedPeers.sorted() {
updateBannedRights[id] = bannedRights
}
}
return AdminUserActionsSheet.ChatResult(
reportSpamPeers: reportSpamPeers,
deleteAllFromPeers: deleteAllFromPeers,
deleteAllReactionsFromPeers: deleteAllReactionsFromPeers,
banPeers: banPeers,
updateBannedRights: updateBannedRights
)
}
private func calculateLiveStreamResult() -> AdminUserActionsSheet.LiveStreamResult {
return AdminUserActionsSheet.LiveStreamResult(
reportSpam: !self.optionReportSelectedPeers.isEmpty,
deleteAll: !self.optionDeleteAllSelectedPeers.isEmpty,
ban: !self.optionBanSelectedPeers.isEmpty
)
}
func update(component: AdminUserActionsSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
let _ = (component.context.account.postbox.peerView(id: component.chatPeer.id)
|> take(1)).start(next: { [weak self] peerView in
guard let self else {
return
}
var selectAll = false
if let cachedData = peerView.cachedData as? CachedChannelData {
if let memberCount = cachedData.participantsSummary.memberCount, memberCount >= 1000 {
selectAll = true
} else if case let .known(peerId) = cachedData.linkedDiscussionPeerId, let _ = peerId {
selectAll = true
}
}
if selectAll && !"".isEmpty {
var selectedPeers = Set<EnginePeer.Id>()
for peer in component.peers {
selectedPeers.insert(peer.peer.id)
}
self.optionReportSelectedPeers = selectedPeers
self.optionDeleteAllSelectedPeers = selectedPeers
self.optionBanSelectedPeers = selectedPeers
}
if !self.isUpdating {
self.state?.updated()
}
})
var (allowedParticipantRights, allowedMediaRights) = rightsFromBannedRights([])
if case let .channel(channel) = component.chatPeer {
(allowedParticipantRights, allowedMediaRights) = rightsFromBannedRights(channel.defaultBannedRights?.flags ?? [])
}
var (commonParticipantRights, commonMediaRights) = rightsFromBannedRights([])
loop: for peer in component.peers {
var (peerParticipantRights, peerMediaRights) = rightsFromBannedRights([])
switch peer.participant {
case .creator:
allowedParticipantRights = []
allowedMediaRights = []
break loop
case let .member(_, _, adminInfo, banInfo, _, _):
if adminInfo != nil {
(allowedParticipantRights, allowedMediaRights) = rightsFromBannedRights([])
break loop
} else if let banInfo {
(peerParticipantRights, peerMediaRights) = rightsFromBannedRights(banInfo.rights.flags)
}
}
peerParticipantRights = peerParticipantRights.intersection(allowedParticipantRights)
peerMediaRights = peerMediaRights.intersection(allowedMediaRights)
commonParticipantRights = commonParticipantRights.intersection(peerParticipantRights)
commonMediaRights = commonMediaRights.intersection(peerMediaRights)
}
commonParticipantRights = commonParticipantRights.intersection(allowedParticipantRights)
commonMediaRights = commonMediaRights.intersection(allowedMediaRights)
self.allowedParticipantRights = allowedParticipantRights
self.participantRights = commonParticipantRights
self.allowedMediaRights = allowedMediaRights
self.mediaRights = commonMediaRights
}
self.component = component
self.state = state
let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environmentValue
let controller = environmentValue.controller
let theme = environmentValue.theme.withModalBlocksBackground()
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
let dismiss: (Bool) -> Void = { [weak self] animated in
guard let self, !self.isDismissing else {
return
}
self.isDismissing = true
let performDismiss: () -> Void = {
if let controller = controller() as? AdminUserActionsSheet {
controller.completePendingDismiss()
controller.dismiss(animated: false)
}
}
if animated {
self.animateOut.invoke(Action { _ in
performDismiss()
})
} else {
performDismiss()
}
}
let disableOptionsSectionAnimation = self.previousWasConfigurationExpanded != self.isConfigurationExpanded
self.previousWasConfigurationExpanded = self.isConfigurationExpanded
let currentState = AdminUserActionsSheetState(
isOptionReportExpanded: self.isOptionReportExpanded,
optionReportSelectedPeers: self.optionReportSelectedPeers,
isOptionDeleteAllExpanded: self.isOptionDeleteAllExpanded,
optionDeleteAllSelectedPeers: self.optionDeleteAllSelectedPeers,
optionDeleteAllReactionsSelectedPeers: self.optionDeleteAllSelectedPeers,
isOptionBanExpanded: self.isOptionBanExpanded,
optionBanSelectedPeers: self.optionBanSelectedPeers,
isConfigurationExpanded: self.isConfigurationExpanded,
isMediaSectionExpanded: self.isMediaSectionExpanded,
allowedParticipantRights: self.allowedParticipantRights,
allowedMediaRights: self.allowedMediaRights,
participantRights: self.participantRights,
mediaRights: self.mediaRights
)
let performMainAction: () -> Void = { [weak self] in
guard let self, let component = self.component else {
return
}
let monoforumResult = self.calculateMonoforumResult()
let chatResult = self.calculateChatResult()
let liveStreamResult = self.calculateLiveStreamResult()
dismiss(true)
switch component.mode {
case let .monoforum(completion):
completion(monoforumResult)
case let .chat(_, _, completion):
completion(chatResult)
case let .liveStream(_, _, completion):
completion(liveStreamResult)
case let .chatReaction(completion):
completion(chatResult)
}
}
let sheetSize = self.sheet.update(
transition: transition,
component: AnyComponent(ResizableSheetComponent<ViewControllerComponentContainer.Environment>(
content: AnyComponent<ViewControllerComponentContainer.Environment>(AdminUserActionsContentComponent(
context: component.context,
chatPeer: component.chatPeer,
peers: component.peers,
mode: component.mode,
theme: theme,
strings: environmentValue.strings,
presentationData: presentationData,
sheetState: currentState,
disableOptionsSectionAnimation: disableOptionsSectionAnimation,
toggleOptionSelection: { [weak self] section in
guard let self, let component = self.component else {
return
}
var selectedPeers: Set<EnginePeer.Id>
switch section {
case .report:
selectedPeers = self.optionReportSelectedPeers
case .deleteAll:
selectedPeers = self.optionDeleteAllSelectedPeers
case .ban:
selectedPeers = self.optionBanSelectedPeers
}
if selectedPeers.isEmpty {
for peer in component.peers {
selectedPeers.insert(peer.peer.id)
}
} else {
selectedPeers.removeAll()
}
switch section {
case .report:
self.optionReportSelectedPeers = selectedPeers
case .deleteAll:
self.optionDeleteAllSelectedPeers = selectedPeers
case .ban:
self.optionBanSelectedPeers = selectedPeers
if self.isConfigurationExpanded && self.optionBanSelectedPeers.isEmpty {
self.isConfigurationExpanded = false
}
}
self.state?.updated(transition: .spring(duration: 0.35))
},
toggleOptionExpansion: { [weak self] section in
guard let self else {
return
}
switch section {
case .report:
self.isOptionReportExpanded = !self.isOptionReportExpanded
case .deleteAll:
self.isOptionDeleteAllExpanded = !self.isOptionDeleteAllExpanded
case .ban:
self.isOptionBanExpanded = !self.isOptionBanExpanded
}
self.state?.updated(transition: .spring(duration: 0.35))
},
togglePeerSelection: { [weak self] section, peer in
guard let self else {
return
}
var selectedPeers: Set<EnginePeer.Id>
switch section {
case .report:
selectedPeers = self.optionReportSelectedPeers
case .deleteAll:
selectedPeers = self.optionDeleteAllSelectedPeers
case .ban:
selectedPeers = self.optionBanSelectedPeers
}
if selectedPeers.contains(peer.id) {
selectedPeers.remove(peer.id)
} else {
selectedPeers.insert(peer.id)
}
switch section {
case .report:
self.optionReportSelectedPeers = selectedPeers
case .deleteAll:
self.optionDeleteAllSelectedPeers = selectedPeers
case .ban:
self.optionBanSelectedPeers = selectedPeers
}
self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)))
},
toggleConfiguration: { [weak self] in
guard let self, let component = self.component else {
return
}
self.isConfigurationExpanded = !self.isConfigurationExpanded
if self.isConfigurationExpanded && self.optionBanSelectedPeers.isEmpty {
for peer in component.peers {
self.optionBanSelectedPeers.insert(peer.peer.id)
}
}
self.state?.updated(transition: .spring(duration: 0.35))
},
toggleConfigItem: { [weak self] configItem in
guard let self else {
return
}
switch configItem {
case .sendMessages:
if self.participantRights.contains(.sendMessages) {
self.participantRights.remove(.sendMessages)
} else {
self.participantRights.insert(.sendMessages)
}
case .sendMedia:
if self.mediaRights.isEmpty {
self.mediaRights = self.allowedMediaRights
} else {
self.mediaRights = []
}
case .addUsers:
if self.participantRights.contains(.addMembers) {
self.participantRights.remove(.addMembers)
} else {
self.participantRights.insert(.addMembers)
}
case .pinMessages:
if self.participantRights.contains(.pinMessages) {
self.participantRights.remove(.pinMessages)
} else {
self.participantRights.insert(.pinMessages)
}
case .changeInfo:
if self.participantRights.contains(.changeInfo) {
self.participantRights.remove(.changeInfo)
} else {
self.participantRights.insert(.changeInfo)
}
}
self.state?.updated(transition: .spring(duration: 0.35))
},
toggleMediaSectionExpansion: { [weak self] in
guard let self else {
return
}
self.isMediaSectionExpanded = !self.isMediaSectionExpanded
self.state?.updated(transition: .spring(duration: 0.35))
},
toggleMediaRight: { [weak self] mediaRight in
guard let self else {
return
}
if self.mediaRights.contains(mediaRight) {
self.mediaRights.remove(mediaRight)
} else {
self.mediaRights.insert(mediaRight)
}
self.state?.updated(transition: .spring(duration: 0.35))
}
)),
titleItem: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: adminUserActionsTitle(
strings: environmentValue.strings,
mode: component.mode,
peers: component.peers,
selectedDeleteAllPeers: self.optionDeleteAllSelectedPeers
),
font: Font.semibold(17.0),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
leftItem: AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
dismiss(true)
}
)
),
bottomItem: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
cornerRadius: 54.0 * 0.5
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(ButtonTextContentComponent(
text: environmentValue.strings.Chat_AdminActionSheet_ActionButton,
badge: 0,
textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor,
badgeForeground: theme.list.itemCheckColors.fillColor
))
),
isEnabled: true,
displaysProgress: false,
action: {
performMainAction()
}
)),
backgroundColor: .color(theme.list.modalBlocksBackgroundColor),
animateOut: self.animateOut
)),
environment: {
environmentValue
ResizableSheetComponentEnvironment(
theme: theme,
statusBarHeight: environmentValue.statusBarHeight,
safeInsets: environmentValue.safeInsets,
inputHeight: 0.0,
metrics: environmentValue.metrics,
deviceMetrics: environmentValue.deviceMetrics,
isDisplaying: environmentValue.isVisible,
isCentered: environmentValue.metrics.widthClass == .regular,
screenSize: availableSize,
regularMetricsSize: nil,
dismiss: { animated in
dismiss(animated)
}
)
},
forceUpdate: true,
containerSize: availableSize
)
self.sheet.parentState = state
if let sheetView = self.sheet.view {
if sheetView.superview == nil {
self.addSubview(sheetView)
}
transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize))
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class AdminUserActionsSheet: ViewControllerComponentContainer {
public enum Mode {
case chat(messageCount: Int, deleteAllMessageCount: Int?, completion: (ChatResult) -> Void)
case liveStream(messageCount: Int, deleteAllMessageCount: Int?, completion: (LiveStreamResult) -> Void)
case monoforum(completion: (MonoforumResult) -> Void)
case chatReaction(completion: (ChatResult) -> Void)
}
public final class ChatResult {
public let reportSpamPeers: [EnginePeer.Id]
public let deleteAllFromPeers: [EnginePeer.Id]
public let deleteAllReactionsFromPeers: [EnginePeer.Id]
public let banPeers: [EnginePeer.Id]
public let updateBannedRights: [EnginePeer.Id: TelegramChatBannedRights]
init(reportSpamPeers: [EnginePeer.Id], deleteAllFromPeers: [EnginePeer.Id], deleteAllReactionsFromPeers: [EnginePeer.Id], banPeers: [EnginePeer.Id], updateBannedRights: [EnginePeer.Id: TelegramChatBannedRights]) {
self.reportSpamPeers = reportSpamPeers
self.deleteAllFromPeers = deleteAllFromPeers
self.deleteAllReactionsFromPeers = deleteAllReactionsFromPeers
self.banPeers = banPeers
self.updateBannedRights = updateBannedRights
}
}
public final class LiveStreamResult {
public let reportSpam: Bool
public let deleteAll: Bool
public let ban: Bool
init(reportSpam: Bool, deleteAll: Bool, ban: Bool) {
self.reportSpam = reportSpam
self.deleteAll = deleteAll
self.ban = ban
}
}
public final class MonoforumResult {
public let ban: Bool
public let reportSpam: Bool
init(ban: Bool, reportSpam: Bool) {
self.ban = ban
self.reportSpam = reportSpam
}
}
private let context: AccountContext
private var isDismissed: Bool = false
private var dismissCompletion: (() -> Void)?
public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], mode: Mode, customTheme: PresentationTheme? = nil) {
self.context = context
super.init(
context: context,
component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, mode: mode),
navigationBarAppearance: .none,
theme: customTheme.flatMap({ .custom($0) }) ?? .default
)
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
}
fileprivate func completePendingDismiss() {
let dismissCompletion = self.dismissCompletion
self.dismissCompletion = nil
dismissCompletion?()
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
self.dismissCompletion = completion
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
} else {
self.completePendingDismiss()
self.dismiss(animated: false)
}
}
}
}
private let optionExpandUsersIcon: UIImage? = {
let sourceImage = UIImage(bundleImageName: "Item List/InlineIconUsers")!
return generateImage(CGSize(width: sourceImage.size.width, height: sourceImage.size.height), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
sourceImage.draw(at: CGPoint(x: 0.0, y: 0.0))
UIGraphicsPopContext()
})!.precomposed().withRenderingMode(.alwaysTemplate)
}()
private final class OptionSectionExpandIndicatorComponent: Component {
let theme: PresentationTheme
let count: Int
let isExpanded: Bool
init(
theme: PresentationTheme,
count: Int,
isExpanded: Bool
) {
self.theme = theme
self.count = count
self.isExpanded = isExpanded
}
static func ==(lhs: OptionSectionExpandIndicatorComponent, rhs: OptionSectionExpandIndicatorComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.count != rhs.count {
return false
}
if lhs.isExpanded != rhs.isExpanded {
return false
}
return true
}
final class View: UIView {
private let iconView: UIImageView
private let arrowView: UIImageView
private let count = ComponentView<Empty>()
override init(frame: CGRect) {
self.iconView = UIImageView()
self.arrowView = UIImageView()
super.init(frame: frame)
self.addSubview(self.iconView)
self.addSubview(self.arrowView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: OptionSectionExpandIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let countArrowSpacing: CGFloat = 1.0
let iconCountSpacing: CGFloat = 1.0
if self.iconView.image == nil {
self.iconView.image = optionExpandUsersIcon
}
self.iconView.tintColor = component.theme.list.itemPrimaryTextColor
let iconSize = self.iconView.image?.size ?? CGSize(width: 12.0, height: 12.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 countSize = self.count.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "\(component.count)", font: Font.semibold(13.0), textColor: component.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let size = CGSize(width: 60.0, height: availableSize.height)
let arrowFrame = CGRect(origin: CGPoint(x: size.width - arrowSize.width - 12.0, y: floor((size.height - arrowSize.height) * 0.5)), size: arrowSize)
let countFrame = CGRect(origin: CGPoint(x: arrowFrame.minX - countArrowSpacing - countSize.width, y: floor((size.height - countSize.height) * 0.5)), size: countSize)
let iconFrame = CGRect(origin: CGPoint(x: countFrame.minX - iconCountSpacing - iconSize.width, y: floor((size.height - iconSize.height) * 0.5)), size: iconSize)
if let countView = self.count.view {
if countView.superview == nil {
self.addSubview(countView)
}
countView.frame = countFrame
}
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 : 0.0, 0.0))
self.iconView.frame = iconFrame
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
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: ComponentTransition) -> 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: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class OptionsSectionFooterComponent: Component {
let theme: PresentationTheme
let text: String
let fontSize: CGFloat
let isExpanded: Bool
init(
theme: PresentationTheme,
text: String,
fontSize: CGFloat,
isExpanded: Bool
) {
self.theme = theme
self.text = text
self.fontSize = fontSize
self.isExpanded = isExpanded
}
static func ==(lhs: OptionsSectionFooterComponent, rhs: OptionsSectionFooterComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.fontSize != rhs.fontSize {
return false
}
if lhs.isExpanded != rhs.isExpanded {
return false
}
return true
}
final class View: UIView {
private let arrowView: UIImageView
private let textView: ImmediateTextView
override init(frame: CGRect) {
self.arrowView = UIImageView()
self.textView = ImmediateTextView()
self.textView.maximumNumberOfLines = 0
super.init(frame: frame)
self.addSubview(self.arrowView)
self.addSubview(self.textView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: OptionsSectionFooterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
if self.arrowView.image == nil {
self.arrowView.image = PresentationResourcesItemList.expandSmallDownArrowImage(component.theme)
}
self.arrowView.tintColor = component.theme.list.itemAccentColor
let arrowSize = self.arrowView.image?.size ?? CGSize(width: 1.0, height: 1.0)
let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: component.text, font: Font.regular(component.fontSize), textColor: component.theme.list.itemAccentColor))
attributedText.append(NSAttributedString(string: ">", font: Font.regular(component.fontSize), textColor: .clear))
self.textView.attributedText = attributedText
let textLayout = self.textView.updateLayoutFullInfo(availableSize)
let size = textLayout.size
let textFrame = CGRect(origin: CGPoint(), size: textLayout.size)
self.textView.frame = textFrame
var arrowFrame = CGRect()
if let lineRect = textLayout.linesRects().last {
arrowFrame = CGRect(origin: CGPoint(x: textFrame.minX + lineRect.maxX - arrowSize.width + 6.0, y: textFrame.minY + lineRect.maxY - lineRect.height - arrowSize.height - 1.0), size: arrowSize)
}
self.arrowView.center = arrowFrame.center
self.arrowView.bounds = CGRect(origin: CGPoint(), size: arrowFrame.size)
transition.setTransform(view: self.arrowView, transform: CATransform3DMakeRotation(component.isExpanded ? CGFloat.pi : 0.0, 0.0, 0.0, 1.0))
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}