import Foundation import UIKit import TelegramPresentationData import AccountContext import TelegramCore import SwiftSignalKit import Display import TelegramPresentationData import PresentationDataUtils import UndoUI import AdminUserActionsSheet import ContextUI import TelegramStringFormatting import StorageUsageScreen import SettingsUI import DeleteChatPeerActionSheetItem import OverlayStatusController fileprivate struct InitialBannedRights { var value: TelegramChatBannedRights? } extension ChatControllerImpl { fileprivate func applyAdminUserActionsResult(messageIds: Set, reactionPeerId: EnginePeer.Id? = nil, result: AdminUserActionsSheet.ChatResult, initialUserBannedRights: [EnginePeer.Id: InitialBannedRights]) { guard let messagesPeerId = self.chatLocation.peerId else { return } guard let banLocationPeerId = self.presentationInterfaceState.renderedPeer?.chatOrMonoforumMainPeer?.id else { return } var title: String? if let _ = reactionPeerId { title = self.presentationData.strings.Chat_AdminAction_ToastReactionsDeletedTitleSingle } else { title = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple } if !result.deleteAllFromPeers.isEmpty { title = self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple } var text: String = "" var undoRights: [EnginePeer.Id: InitialBannedRights] = [:] if !result.reportSpamPeers.isEmpty { if !text.isEmpty { text.append("\n") } text.append(self.presentationData.strings.Chat_AdminAction_ToastReportedSpamText(Int32(result.reportSpamPeers.count))) } if !result.banPeers.isEmpty { if !text.isEmpty { text.append("\n") } text.append(self.presentationData.strings.Chat_AdminAction_ToastBannedText(Int32(result.banPeers.count))) for id in result.banPeers { if let value = initialUserBannedRights[id] { undoRights[id] = value } } } if !result.updateBannedRights.isEmpty { if !text.isEmpty { text.append("\n") } text.append(self.presentationData.strings.Chat_AdminAction_ToastRestrictedText(Int32(result.updateBannedRights.count))) for (id, _) in result.updateBannedRights { if let value = initialUserBannedRights[id] { undoRights[id] = value } } } do { if let reactionPeerId { if let messageId = messageIds.first { let _ = self.context.engine.messages.deleteReaction(messageId: messageId, authorId: reactionPeerId).startStandalone() } } else { let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } for authorId in result.deleteAllFromPeers { let _ = self.context.engine.messages.deleteAllMessagesWithAuthor(peerId: messagesPeerId, authorId: authorId, namespace: Namespaces.Message.Cloud).startStandalone() let _ = self.context.engine.messages.clearAuthorHistory(peerId: messagesPeerId, memberId: authorId).startStandalone() } let aroundMessageId = messageIds.count == 1 ? messageIds.first : nil for authorId in result.deleteAllReactionsFromPeers { let _ = self.context.engine.messages.deleteAllReactionsWithAuthor(peerId: messagesPeerId, authorId: authorId, aroundMessageId: aroundMessageId).startStandalone() } for authorId in result.reportSpamPeers { let _ = self.context.engine.peers.reportPeer(peerId: authorId, reason: .spam, message: "").startStandalone() } for authorId in result.banPeers { let _ = self.context.engine.peers.removePeerMember(peerId: banLocationPeerId, memberId: authorId).startStandalone() } for (authorId, rights) in result.updateBannedRights { let _ = self.context.engine.peers.updateChannelMemberBannedRights(peerId: banLocationPeerId, memberId: authorId, rights: rights).startStandalone() } } if text.isEmpty { if let _ = reactionPeerId { text = self.presentationData.strings.Chat_AdminAction_ToastReactionsDeletedTextSingle } else { text = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple } if !result.deleteAllFromPeers.isEmpty && !result.deleteAllReactionsFromPeers.isEmpty { text = self.presentationData.strings.Chat_AdminAction_ToastMessagesAndReactionsDeletedText } else if !result.deleteAllFromPeers.isEmpty { text = self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple } else if !result.deleteAllReactionsFromPeers.isEmpty { text = self.presentationData.strings.Chat_AdminAction_ToastReactionsDeletedTextMultiple } title = nil } self.present( UndoOverlayController( presentationData: self.presentationData, content: undoRights.isEmpty ? .actionSucceeded(title: title, text: text, cancel: nil, destructive: false) : .removedChat(context: self.context, title: NSAttributedString(string: title ?? text), text: title == nil ? nil : text), elevatedLayout: false, action: { [weak self] action in guard let self else { return true } switch action { case .commit: break case .undo: for (authorId, rights) in initialUserBannedRights { let _ = self.context.engine.peers.updateChannelMemberBannedRights(peerId: banLocationPeerId, memberId: authorId, rights: rights.value).startStandalone() } default: break } return true } ), in: .current ) self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) } func presentMultiBanMessageOptions(accountPeerId: EnginePeer.Id, authors: [EnginePeer], messageIds: Set, options: ChatAvailableMessageActionOptions) { guard let peerId = self.chatLocation.peerId else { return } var deleteAllMessageCount: Signal = .single(nil) if authors.count == 1 { deleteAllMessageCount = self.context.engine.messages.searchMessages(location: .peer(peerId: peerId, fromId: authors[0].id, tags: nil, reactions: nil, threadId: self.chatLocation.threadId, minDate: nil, maxDate: nil), query: "", state: nil) |> map { result, _ -> Int? in return Int(result.totalCount) } } var signal = combineLatest(authors.map { author in self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id) |> map { result -> (EnginePeer, ChannelParticipant?) in return (author, result) } }) let disposables = MetaDisposable() self.navigationActionDisposable.set(disposables) var cancelImpl: (() -> Void)? let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let progressSignal = Signal { [weak self] subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) self?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() } } } |> runOn(Queue.mainQueue()) |> delay(0.3, queue: Queue.mainQueue()) let progressDisposable = progressSignal.startStrict() signal = signal |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() } } cancelImpl = { disposables.set(nil) } disposables.set((combineLatest(signal, deleteAllMessageCount) |> deliverOnMainQueue).startStrict(next: { [weak self] authorsAndParticipants, deleteAllMessageCount in guard let self else { return } let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) |> deliverOnMainQueue).startStandalone(next: { [weak self] chatPeer in guard let self, let chatPeer else { return } var renderedParticipants: [RenderedChannelParticipant] = [] var initialUserBannedRights: [EnginePeer.Id: InitialBannedRights] = [:] for (author, maybeParticipant) in authorsAndParticipants { let participant: ChannelParticipant if let maybeParticipant { participant = maybeParticipant } else { participant = .member(id: author.id, invitedAt: 0, adminInfo: nil, banInfo: ChannelParticipantBannedInfo( rights: TelegramChatBannedRights( flags: [.banReadMessages], untilDate: Int32.max ), restrictedBy: self.context.account.peerId, timestamp: 0, isMember: false ), rank: nil, subscriptionUntilDate: nil) } renderedParticipants.append(RenderedChannelParticipant( participant: participant, peer: author )) switch participant { case .creator: break case let .member(_, _, _, banInfo, _, _): if let banInfo { initialUserBannedRights[participant.peerId] = InitialBannedRights(value: banInfo.rights) } else { initialUserBannedRights[participant.peerId] = InitialBannedRights(value: nil) } } } self.push(AdminUserActionsSheet( context: self.context, chatPeer: chatPeer, peers: renderedParticipants, mode: .chat( messageCount: messageIds.count, deleteAllMessageCount: deleteAllMessageCount, completion: { [weak self] result in guard let self else { return } self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights) } ) )) }) })) } public func presentReactionDeletionOptions(author: EnginePeer, messageId: EngineMessage.Id) { guard self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudChannel, author.id != self.context.account.peerId else { return } let _ = (self.context.sharedContext.chatAvailableMessageActions( engine: self.context.engine, accountPeerId: self.context.account.peerId, messageIds: Set([messageId]), keepUpdated: false ) |> deliverOnMainQueue).startStandalone(next: { [weak self] actions in guard let self, !actions.options.isEmpty else { return } self.presentBanMessageOptions( accountPeerId: self.context.account.peerId, author: author, messageIds: Set([messageId]), options: actions.options, reaction: true ) }) } func presentBanMessageOptions(accountPeerId: EnginePeer.Id, author: EnginePeer, messageIds: Set, options: ChatAvailableMessageActionOptions, reaction: Bool = false) { guard let peerId = self.chatLocation.peerId else { return } var signal = self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id) let disposables = MetaDisposable() self.navigationActionDisposable.set(disposables) var cancelImpl: (() -> Void)? let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let progressSignal = Signal { [weak self] subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) self?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() } } } |> runOn(Queue.mainQueue()) |> delay(0.3, queue: Queue.mainQueue()) let progressDisposable = progressSignal.startStrict() signal = signal |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() } } cancelImpl = { disposables.set(nil) } var deleteAllMessageCount: Signal = .single(nil) do { deleteAllMessageCount = self.context.engine.messages.getSearchMessageCount(location: .peer(peerId: peerId, fromId: author.id, tags: nil, reactions: nil, threadId: self.chatLocation.threadId, minDate: nil, maxDate: nil), query: "") |> map { result -> Int? in return result } } disposables.set((combineLatest(signal, deleteAllMessageCount) |> deliverOnMainQueue).startStrict(next: { [weak self] maybeParticipant, deleteAllMessageCount in guard let self else { return } let participant: ChannelParticipant if let maybeParticipant { participant = maybeParticipant } else { participant = .member(id: author.id, invitedAt: 0, adminInfo: nil, banInfo: ChannelParticipantBannedInfo( rights: TelegramChatBannedRights( flags: [.banReadMessages], untilDate: Int32.max ), restrictedBy: self.context.account.peerId, timestamp: 0, isMember: false ), rank: nil, subscriptionUntilDate: nil) } let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), TelegramEngine.EngineData.Item.Peer.Peer(id: author.id) ) |> deliverOnMainQueue).startStandalone(next: { [weak self] chatPeer, authorPeer in guard let self, let chatPeer else { return } guard let authorPeer else { return } var initialUserBannedRights: [EnginePeer.Id: InitialBannedRights] = [:] switch participant { case .creator: break case let .member(_, _, _, banInfo, _, _): if let banInfo { initialUserBannedRights[participant.peerId] = InitialBannedRights(value: banInfo.rights) } else { initialUserBannedRights[participant.peerId] = InitialBannedRights(value: nil) } } let mode: AdminUserActionsSheet.Mode if reaction { mode = .chatReaction(completion: { [weak self] result in guard let self else { return } self.applyAdminUserActionsResult(messageIds: messageIds, reactionPeerId: authorPeer.id, result: result, initialUserBannedRights: initialUserBannedRights) }) } else { mode = .chat( messageCount: messageIds.count, deleteAllMessageCount: deleteAllMessageCount, completion: { [weak self] result in guard let self else { return } self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights) } ) } self.push(AdminUserActionsSheet( context: self.context, chatPeer: chatPeer, peers: [RenderedChannelParticipant( participant: participant, peer: authorPeer )], mode: mode )) }) })) } func beginDeleteMessagesWithUndo(messageIds: Set, type: InteractiveMessagesDeletionType) { var deleteImmediately = false if case .forEveryone = type { deleteImmediately = true } else if case .scheduledMessages = self.presentationInterfaceState.subject { deleteImmediately = true } else if case .peer(self.context.account.peerId) = self.chatLocation { deleteImmediately = true } if deleteImmediately { let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: type).startStandalone() return } self.chatDisplayNode.historyNode.ignoreMessageIds = Set(messageIds) let undoTitle = self.presentationData.strings.Chat_MessagesDeletedToast_Text(Int32(messageIds.count)) self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: undoTitle), text: nil), elevatedLayout: false, position: .top, action: { [weak self] value in guard let self else { return false } if value == .commit { let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: type).startStandalone() return true } else if value == .undo { self.chatDisplayNode.historyNode.ignoreMessageIds = Set() return true } return false }), in: .current) } func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, sourceView: UIView? = nil, completion: @escaping (ContextMenuActionResult) -> Void) { let _ = (self.context.engine.data.get( EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) ) |> deliverOnMainQueue).start(next: { [weak self] messages in guard let self else { return } if let message = messages.values.compactMap({ $0 }).first(where: { message in message.attributes.contains(where: { $0 is PublishedSuggestedPostMessageAttribute }) }), let attribute = message.attributes.first(where: { $0 is PublishedSuggestedPostMessageAttribute }) as? PublishedSuggestedPostMessageAttribute, message.timestamp > Int32(Date().timeIntervalSince1970) - 60 * 60 * 24 { let commit = { [weak self] in guard let self else { return } let titleString: String let textString: String switch attribute.currency { case .stars: titleString = self.presentationData.strings.Chat_DeletePaidMessageStars_Title textString = self.presentationData.strings.Chat_DeletePaidMessageStars_Text case .ton: titleString = self.presentationData.strings.Chat_DeletePaidMessageTon_Title textString = self.presentationData.strings.Chat_DeletePaidMessageTon_Text } self.present(textAlertController( context: self.context, title: titleString, text: textString, actions: [ TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.Chat_DeletePaidMessage_Action, action: { [weak self] in guard let self else { return } self.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) }), TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}) ], actionLayout: .vertical, parseMarkdown: true ), in: .window(.root)) } if let contextController { contextController.dismiss(completion: commit) } else { commit() } return } if messageIds.count == 1, let message = messages.values.compactMap({ $0 }).first, let repeatAttribute = message.attributes.first(where: { $0 is ScheduledRepeatAttribute }) as? ScheduledRepeatAttribute { let commit = { [weak self] in guard let self else { return } let title: String let text: String let deleteOneAction: String let deleteAllAction: String if message.id.peerId == self.context.account.peerId { title = self.presentationData.strings.Reminders_DeleteRepeatingTitle text = self.presentationData.strings.Reminders_DeleteRepeatingText deleteOneAction = self.presentationData.strings.Reminders_DeleteRepeatingActionSingle deleteAllAction = self.presentationData.strings.Reminders_DeleteRepeatingActionMultiple } else { title = self.presentationData.strings.ScheduledMessages_DeleteRepeatingTitle text = self.presentationData.strings.ScheduledMessages_DeleteRepeatingText deleteOneAction = self.presentationData.strings.ScheduledMessages_DeleteRepeatingActionSingle deleteAllAction = self.presentationData.strings.ScheduledMessages_DeleteRepeatingActionMultiple } self.present(textAlertController( context: self.context, title: title, text: text, actions: [ TextAlertAction(type: .destructiveAction, title: deleteOneAction, action: { [weak self] in guard let self else { return } var entities: TextEntitiesMessageAttribute? var richText: RichTextMessageAttribute? for attribute in message.attributes { if let attribute = attribute as? TextEntitiesMessageAttribute { entities = attribute } else if let attribute = attribute as? RichTextMessageAttribute { richText = attribute } } let scheduleTime = message.timestamp + repeatAttribute.repeatPeriod self.editMessageDisposable.set((self.context.engine.messages.requestEditMessage(messageId: message.id, text: message.text, media: .keep, entities: entities, richText: richText, inlineStickers: [:], webpagePreviewAttribute: nil, disableUrlPreview: false, scheduleInfoAttribute: OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime, repeatPeriod: repeatAttribute.repeatPeriod)) |> deliverOnMainQueue).startStrict(next: { result in }, error: { error in })) }), TextAlertAction(type: .destructiveAction, title: deleteAllAction, action: { [weak self] in guard let self else { return } self.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) }), TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}) ], actionLayout: .vertical, parseMarkdown: true ), in: .window(.root)) } if let contextController { contextController.dismiss(completion: commit) } else { commit() } return } let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] var personalPeerName: String? var isChannel = false if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser { personalPeerName = EnginePeer(user).compactDisplayTitle } else if let peer = self.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let associatedPeerId = peer.associatedPeerId, let user = self.presentationInterfaceState.renderedPeer?.peers[associatedPeerId] as? TelegramUser { personalPeerName = EnginePeer(user).compactDisplayTitle } else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info { isChannel = true } var contextItems: [ContextMenuItem] = [] var canDisplayContextMenu = true if options.contains(.cancelSending) { contextItems.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCancelSending, textColor: .destructive, icon: { _ in nil }, action: { [weak self] _, f in f(.dismissWithoutContent) if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) strongSelf.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) } }))) items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ContextMenuCancelSending, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) strongSelf.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) } })) } var unsendPersonalMessages = false if options.contains(.unsendPersonal) { canDisplayContextMenu = false items.append(ActionSheetTextItem(title: self.presentationData.strings.Chat_UnsendMyMessagesAlertTitle(personalPeerName ?? "").string)) items.append(ActionSheetSwitchItem(title: self.presentationData.strings.Chat_UnsendMyMessages, isOn: false, action: { value in unsendPersonalMessages = value })) } else if options.contains(.deleteGlobally) { let globalTitle: String if isChannel { globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone } else if let personalPeerName = personalPeerName { globalTitle = self.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).string } else { globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone } contextItems.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { [weak self] c, f in if let strongSelf = self { var giveaway: TelegramMediaGiveaway? for messageId in messageIds { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId)?._asMessage() { if let media = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway { giveaway = media break } } } let commit = { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) strongSelf.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) } if let giveaway { let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) if currentTime < giveaway.untilDate { Queue.mainQueue().after(0.2) { let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings) strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Title, text: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Text(dateString).string, actions: [TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Common_Delete, action: { commit() }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { })], parseMarkdown: true), in: .window(.root)) } f(.default) } else { f(.dismissWithoutContent) commit() } } else { if "".isEmpty { f(.dismissWithoutContent) commit() } else { c?.dismiss(completion: { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { commit() }) }) } } } }))) items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) strongSelf.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) } })) } if options.contains(.deleteLocally) { var localOptionText = self.presentationData.strings.Conversation_DeleteMessagesForMe if self.chatLocation.peerId == self.context.account.peerId { if case .scheduledMessages = self.presentationInterfaceState.subject { localOptionText = messageIds.count > 1 ? self.presentationData.strings.ScheduledMessages_Reminder_DeleteMany : self.presentationData.strings.ScheduledMessages_Reminder_Delete } else if case .peer(self.context.account.peerId) = self.chatLocation, messages.values.allSatisfy({ message in message?._asMessage().effectivelyIncoming(self.context.account.peerId) ?? false }) { localOptionText = self.presentationData.strings.Chat_ConfirmationRemoveFromSavedMessages } else { localOptionText = self.presentationData.strings.Chat_ConfirmationDeleteFromSavedMessages } } else if case .scheduledMessages = self.presentationInterfaceState.subject { localOptionText = messageIds.count > 1 ? self.presentationData.strings.ScheduledMessages_DeleteMany : self.presentationData.strings.ScheduledMessages_Delete } else { if options.contains(.unsendPersonal) { localOptionText = self.presentationData.strings.Chat_DeleteMessagesConfirmation(Int32(messageIds.count)) } else if case .peer(self.context.account.peerId) = self.chatLocation { if messageIds.count == 1 { localOptionText = self.presentationData.strings.Conversation_Moderate_Delete } else { localOptionText = self.presentationData.strings.Conversation_DeleteManyMessages } } } contextItems.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { [weak self] c, f in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) let commit: () -> Void = { guard let strongSelf = self else { return } strongSelf.beginDeleteMessagesWithUndo(messageIds: messageIds, type: unsendPersonalMessages ? .forEveryone : .forLocalPeer) } if "".isEmpty { f(.dismissWithoutContent) commit() } else { c?.dismiss(completion: { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { commit() }) }) } } }))) items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) strongSelf.beginDeleteMessagesWithUndo(messageIds: messageIds, type: unsendPersonalMessages ? .forEveryone : .forLocalPeer) } })) } if canDisplayContextMenu, let contextController = contextController { contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true) } else if canDisplayContextMenu, let sourceView = sourceView { let contextController = makeContextController( presentationData: self.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: self, sourceView: sourceView, insets: .zero, actionsOnTop: true)), items: .single(ContextController.Items(content: .list(contextItems))), gesture: nil ) self.chatDisplayNode.dismissInput() self.presentInGlobalOverlay(contextController) completion(.default) } else { actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) if let contextController = contextController { contextController.dismiss(completion: { [weak self] in self?.present(actionSheet, in: .window(.root)) }) } else { self.chatDisplayNode.dismissInput() self.present(actionSheet, in: .window(.root)) completion(.default) } } }) } func presentClearCacheSuggestion() { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: EnginePeer(peer), chatPeer: EnginePeer(peer), action: .clearCacheSuggestion, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder)) var presented = false items.append(ActionSheetButtonItem(title: self.presentationData.strings.ClearCache_FreeSpace, color: .accent, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self, !presented { presented = true let context = strongSelf.context strongSelf.push(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in return storageUsageExceptionsScreen(context: context, category: category) })) } })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) self.chatDisplayNode.dismissInput() self.presentInGlobalOverlay(actionSheet) } func openDeleteMonoforumPeer(peerId: EnginePeer.Id) { guard let chatPeerId = self.chatLocation.peerId else { return } guard let mainChannel = self.presentationInterfaceState.renderedPeer?.chatOrMonoforumMainPeer as? TelegramChannel else { return } let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId), TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) |> deliverOnMainQueue).startStandalone(next: { [weak self] chatPeer, authorPeer in guard let self, let chatPeer, let authorPeer else { return } var initialUserBannedRights: [EnginePeer.Id: InitialBannedRights] = [:] initialUserBannedRights[authorPeer.id] = InitialBannedRights(value: nil) let participant: ChannelParticipant = .member(id: authorPeer.id, invitedAt: 0, adminInfo: nil, banInfo: ChannelParticipantBannedInfo( rights: TelegramChatBannedRights(flags: [], untilDate: 0), restrictedBy: self.context.account.peerId, timestamp: 0, isMember: false ), rank: nil, subscriptionUntilDate: nil) self.push(AdminUserActionsSheet( context: self.context, chatPeer: chatPeer, peers: [RenderedChannelParticipant( participant: participant, peer: authorPeer )], mode: .monoforum(completion: { [weak self] result in guard let self else { return } if self.chatLocation.threadId == peerId.toInt64() { self.updateChatLocationThread(threadId: nil) } let _ = self.context.engine.peers.removeForumChannelThread(id: chatPeerId, threadId: peerId.toInt64()).startStandalone(completed: { }) if result.ban { let _ = self.context.engine.peers.updateChannelMemberBannedRights(peerId: mainChannel.id, memberId: peerId, rights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max) ).startStandalone() } if result.reportSpam { let _ = self.context.engine.peers.reportPeer(peerId: peerId, reason: .spam, message: "").startStandalone() } }) )) }) } }