import Foundation import TelegramPresentationData import AccountContext import Postbox import TelegramCore import SwiftSignalKit import Display import TelegramPresentationData import PresentationDataUtils import UndoUI import AdminUserActionsSheet import ContextUI import TelegramStringFormatting import StorageUsageScreen import SettingsUI import DeleteChatPeerActionSheetItem fileprivate struct InitialBannedRights { var value: TelegramChatBannedRights? } extension ChatControllerImpl { fileprivate func applyAdminUserActionsResult(messageIds: Set, result: AdminUserActionsSheet.Result, initialUserBannedRights: [EnginePeer.Id: InitialBannedRights]) { guard let peerId = self.chatLocation.peerId else { return } //TODO:localize var title: String? = messageIds.count == 1 ? "Message Deleted" : "Messages Deleted" var text: String = "" var undoRights: [EnginePeer.Id: InitialBannedRights] = [:] if !result.reportSpamPeers.isEmpty { if !text.isEmpty { text.append("\n") } if result.reportSpamPeers.count == 1 { text.append("**1** user reported for spam.") } else { text.append("**\(result.reportSpamPeers.count)** users reported for spam.") } } if !result.banPeers.isEmpty { if !text.isEmpty { text.append("\n") } if result.banPeers.count == 1 { text.append("**1** user banned.") } else { text.append("**\(result.banPeers.count)** users banned.") } for id in result.banPeers { if let value = initialUserBannedRights[id] { undoRights[id] = value } } } if !result.updateBannedRights.isEmpty { if !text.isEmpty { text.append("\n") } if result.updateBannedRights.count == 1 { text.append("**1** user restricted.") } else { text.append("**\(result.updateBannedRights.count)** users restricted.") } for id in result.banPeers { if let value = initialUserBannedRights[id] { undoRights[id] = value } } } do { let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() for authorId in result.deleteAllFromPeers { let _ = self.context.engine.messages.deleteAllMessagesWithAuthor(peerId: peerId, authorId: authorId, namespace: Namespaces.Message.Cloud).startStandalone() let _ = self.context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: authorId).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: peerId, memberId: authorId).startStandalone() } for (authorId, rights) in result.updateBannedRights { let _ = self.context.engine.peers.updateChannelMemberBannedRights(peerId: peerId, memberId: authorId, rights: rights).startStandalone() } } if text.isEmpty { text = messageIds.count == 1 ? "Message Deleted." : "Messages Deleted." title = nil } self.present( UndoOverlayController( presentationData: self.presentationData, content: undoRights.isEmpty ? .actionSucceeded(title: title, text: text, cancel: nil, destructive: false) : .removedChat(title: 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: peerId, 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: PeerId, authors: [Peer], messageIds: Set, options: ChatAvailableMessageActionOptions) { guard let peerId = self.chatLocation.peerId else { return } self.navigationActionDisposable.set((combineLatest(authors.map { author in self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id) }) |> deliverOnMainQueue).startStrict(next: { [weak self] participants 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 maybeParticipant in participants { guard let participant = maybeParticipant else { continue } guard let peer = authors.first(where: { $0.id == participant.peerId }) else { continue } renderedParticipants.append(RenderedChannelParticipant( participant: participant, peer: peer )) 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, messageCount: messageIds.count, completion: { [weak self] result in guard let self else { return } self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights) } )) }) })) } func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set, options: ChatAvailableMessageActionOptions) { guard let peerId = self.chatLocation.peerId else { return } do { self.navigationActionDisposable.set((self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id) |> deliverOnMainQueue).startStrict(next: { [weak self] participant in if let strongSelf = self { if "".isEmpty { guard let participant else { return } let _ = (strongSelf.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), TelegramEngine.EngineData.Item.Peer.Peer(id: author.id) ) |> deliverOnMainQueue).startStandalone(next: { 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) } } self.push(AdminUserActionsSheet( context: self.context, chatPeer: chatPeer, peers: [RenderedChannelParticipant( participant: participant, peer: authorPeer._asPeer() )], messageCount: messageIds.count, completion: { [weak self] result in guard let self else { return } self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights) } )) }) return } let canBan = participant?.canBeBannedBy(peerId: accountPeerId) ?? true let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] var actions = Set([0]) let toggleCheck: (Int, Int) -> Void = { [weak actionSheet] category, itemIndex in if actions.contains(category) { actions.remove(category) } else { actions.insert(category) } actionSheet?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in if let item = item as? ActionSheetCheckboxItem { return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) } return item }) } var itemIndex = 0 var categories: [Int] = [0] if canBan { categories.append(1) } categories.append(contentsOf: [2, 3]) for categoryId in categories as [Int] { var title = "" if categoryId == 0 { title = strongSelf.presentationData.strings.Conversation_Moderate_Delete } else if categoryId == 1 { title = strongSelf.presentationData.strings.Conversation_Moderate_Ban } else if categoryId == 2 { title = strongSelf.presentationData.strings.Conversation_Moderate_Report } else if categoryId == 3 { title = strongSelf.presentationData.strings.Conversation_Moderate_DeleteAllMessages(EnginePeer(author).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string } let index = itemIndex items.append(ActionSheetCheckboxItem(title: title, label: "", value: actions.contains(categoryId), action: { value in toggleCheck(categoryId, index) })) itemIndex += 1 } items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Done, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) if actions.contains(3) { let _ = strongSelf.context.engine.messages.deleteAllMessagesWithAuthor(peerId: peerId, authorId: author.id, namespace: Namespaces.Message.Cloud).startStandalone() let _ = strongSelf.context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: author.id).startStandalone() } else if actions.contains(0) { let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } if actions.contains(1) { let _ = strongSelf.context.engine.peers.removePeerMember(peerId: peerId, memberId: author.id).startStandalone() } } })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) } })) } } func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, 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 } 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 } if options.contains(.cancelSending) { 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() } }) let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } })) } var contextItems: [ContextMenuItem] = [] var canDisplayContextMenu = true 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) { 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() } }) let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } 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: .defaultAction, 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() } }) let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } })) } if options.contains(.deleteLocally) { var localOptionText = self.presentationData.strings.Conversation_DeleteMessagesForMe if self.chatLocation.peerId == self.context.account.peerId { 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 } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() } 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() } }) let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() } })) } if canDisplayContextMenu, let contextController = contextController { contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true) } 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) } }