From 3895daa97437fc93b83c3c0b77d7eb090cb5e78f Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 21 Nov 2022 20:25:16 +0400 Subject: [PATCH] Add antispam false positive reporting --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + .../Sources/AccountContext.swift | 10 +- .../Peers/ChannelAdminEventLogContext.swift | 17 +- .../Peers/TelegramEnginePeers.swift | 4 + .../AntiSpam.imageset/Contents.json | 12 + .../AntiSpam.imageset/reportfalse_24.pdf | 135 ++++++++++++ .../ChatRecentActionsControllerNode.swift | 205 ++++++++++++++---- .../ChatRecentActionsHistoryTransition.swift | 19 +- 8 files changed, 350 insertions(+), 55 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AntiSpam.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AntiSpam.imageset/reportfalse_24.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5800948e8b..41e48e97c8 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8302,3 +8302,6 @@ Sorry for the inconvenience."; "CreateTopic.ShowGeneral" = "Show in Topics"; "CreateTopic.ShowGeneralInfo" = "If the 'General' topic is hidden, group members can pull down in the topic list to view it."; + +"Conversation.ContextMenuReportFalsePositive" = "Report False Positive"; +"Group.AdminLog.AntiSpamFalsePositiveReportedText" = "Telegram moderators will review your report. Thank you!"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index fa7aec6e2b..82819a11d6 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -925,18 +925,20 @@ public struct PremiumConfiguration { public struct AntiSpamBotConfiguration { public static var defaultValue: AntiSpamBotConfiguration { - return AntiSpamBotConfiguration(antiSpamBotId: nil) + return AntiSpamBotConfiguration(antiSpamBotId: nil, minimumGroupParticipants: 100) } public let antiSpamBotId: EnginePeer.Id? + public let minimumGroupParticipants: Int32 - fileprivate init(antiSpamBotId: EnginePeer.Id?) { + fileprivate init(antiSpamBotId: EnginePeer.Id?, minimumGroupParticipants: Int32) { self.antiSpamBotId = antiSpamBotId + self.minimumGroupParticipants = minimumGroupParticipants } public static func with(appConfiguration: AppConfiguration) -> AntiSpamBotConfiguration { - if let data = appConfiguration.data, let string = data["telegram_antispam_user_id"] as? String, let value = Int64(string) { - return AntiSpamBotConfiguration(antiSpamBotId: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(value))) + if let data = appConfiguration.data, let botIdString = data["telegram_antispam_user_id"] as? String, let botIdValue = Int64(botIdString), let groupSize = data["telegram_antispam_group_size_min"] as? Double { + return AntiSpamBotConfiguration(antiSpamBotId: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(botIdValue)), minimumGroupParticipants: Int32(groupSize)) } else { return .defaultValue } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogContext.swift index 5568220adc..14f66f81a7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogContext.swift @@ -3,6 +3,7 @@ import SwiftSignalKit public struct ChannelAdminEventLogEntry: Comparable { public let stableId: UInt32 + public let headerStableId: UInt32 public let event: AdminLogEvent public let peers: [PeerId: Peer] @@ -85,6 +86,7 @@ public final class ChannelAdminEventLogContext { private var filter: ChannelAdminEventLogFilter = ChannelAdminEventLogFilter() private var nextStableId: UInt32 = 1 + private var headerStableIds: [AdminLogEventId: UInt32] = [:] private var stableIds: [AdminLogEventId: UInt32] = [:] private var entries: ([ChannelAdminEventLogEntry], ChannelAdminEventLogFilter) = ([], ChannelAdminEventLogFilter()) @@ -182,13 +184,13 @@ public final class ChannelAdminEventLogContext { } var entries: [ChannelAdminEventLogEntry] = events.map { event in - return ChannelAdminEventLogEntry(stableId: strongSelf.stableIdForEventId(event.id), event: event, peers: result.peers) + return ChannelAdminEventLogEntry(stableId: strongSelf.stableIdForEventId(event.id), headerStableId: strongSelf.headerStableIdForEventId(event.id), event: event, peers: result.peers) } entries.append(contentsOf: strongSelf.entries.0) strongSelf.entries = (entries, strongSelf.filter) } else { let entries: [ChannelAdminEventLogEntry] = events.map { event in - return ChannelAdminEventLogEntry(stableId: strongSelf.stableIdForEventId(event.id), event: event, peers: result.peers) + return ChannelAdminEventLogEntry(stableId: strongSelf.stableIdForEventId(event.id), headerStableId: strongSelf.headerStableIdForEventId(event.id), event: event, peers: result.peers) } strongSelf.entries = (entries, strongSelf.filter) } @@ -214,4 +216,15 @@ public final class ChannelAdminEventLogContext { return value } } + + private func headerStableIdForEventId(_ id: AdminLogEventId) -> UInt32 { + if let value = self.headerStableIds[id] { + return value + } else { + let value = self.nextStableId + self.nextStableId += 1 + self.headerStableIds[id] = value + return value + } + } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 3e6c12fed7..ce31010556 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -292,6 +292,10 @@ public extension TelegramEngine { return _internal_toggleAntiSpamProtection(account: self.account, peerId: peerId, enabled: enabled) } + public func reportAntiSpamFalsePositive(peerId: PeerId, messageId: MessageId) -> Signal { + return _internal_reportAntiSpamFalsePositive(account: self.account, peerId: peerId, messageId: messageId) + } + public func requestPeerPhotos(peerId: PeerId) -> Signal<[TelegramPeerPhoto], NoError> { return _internal_requestPeerPhotos(postbox: self.account.postbox, network: self.account.network, peerId: peerId) } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AntiSpam.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AntiSpam.imageset/Contents.json new file mode 100644 index 0000000000..46eef6a9dd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AntiSpam.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "reportfalse_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AntiSpam.imageset/reportfalse_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AntiSpam.imageset/reportfalse_24.pdf new file mode 100644 index 0000000000..5a01fb3a88 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AntiSpam.imageset/reportfalse_24.pdf @@ -0,0 +1,135 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.834961 3.335003 cm +0.000000 0.000000 0.000000 scn +5.861066 15.587957 m +6.196899 15.711591 6.498980 15.822798 6.742460 15.912901 c +6.938185 15.972766 7.061687 15.999998 7.169050 15.999998 c +7.276096 15.999998 7.389237 15.973757 7.604651 15.910002 c +8.743544 15.512181 11.235205 14.579237 12.364463 14.129102 c +12.722498 13.984460 12.842218 13.892902 12.896966 13.823884 c +12.934976 13.775966 13.000000 13.665576 13.000000 13.307275 c +13.000000 7.185324 l +13.000000 6.553703 12.915850 6.062992 12.747086 5.640941 c +12.579326 5.221397 12.313388 4.833261 11.900326 4.426327 c +11.044427 3.583125 9.640606 2.731597 7.372818 1.412622 c +7.318006 1.381764 7.263755 1.358576 7.218547 1.344114 c +7.193927 1.336239 7.177481 1.332685 7.169050 1.331110 c +7.160619 1.332685 7.144172 1.336239 7.119552 1.344114 c +7.074736 1.358451 7.021033 1.381364 6.966707 1.411822 c +4.705894 2.743835 3.299533 3.594983 2.437976 4.437263 c +2.022268 4.843668 1.754397 5.229964 1.585310 5.647222 c +1.415345 6.066645 1.330000 6.554857 1.330000 7.185324 c +1.330000 13.307275 l +1.330000 13.662965 1.395993 13.779277 1.436774 13.830870 c +1.491529 13.900139 1.610740 13.992070 1.957460 14.125925 c +1.957469 14.125902 l +1.962718 14.127980 l +2.859330 14.482901 4.598611 15.123198 5.860996 15.587932 c +5.861066 15.587957 l +h +7.169050 17.329998 m +6.842114 17.329998 6.548457 17.244995 6.332937 17.178432 c +6.321291 17.174837 6.309747 17.170919 6.298316 17.166689 c +6.058186 17.077799 5.755209 16.966265 5.415772 16.841309 c +5.415587 16.841240 l +4.154459 16.376986 2.390144 15.727495 1.475762 15.365632 c +1.086233 15.215069 0.680177 15.018452 0.393381 14.655631 c +0.091951 14.274296 0.000000 13.813374 0.000000 13.307275 c +0.000000 7.185324 l +0.000000 6.433676 0.101960 5.766401 0.352673 5.147715 c +0.604264 4.526862 0.990442 3.992426 1.508224 3.486230 c +2.512969 2.503967 4.089810 1.563107 6.296909 0.262775 c +6.296864 0.262699 l +6.306323 0.257332 l +6.547542 0.120480 6.865578 0.000000 7.169050 0.000000 c +7.472521 0.000000 7.790557 0.120480 8.031776 0.257332 c +8.031796 0.257298 l +8.037958 0.260883 l +10.255331 1.550500 11.831186 2.491209 12.833723 3.478873 c +13.350101 3.987590 13.733150 4.524757 13.982018 5.147135 c +14.229882 5.767005 14.330000 6.434830 14.330000 7.185324 c +14.330000 13.307275 l +14.330000 13.810762 14.241129 14.269478 13.938952 14.650421 c +13.653583 15.010178 13.247056 15.207094 12.860974 15.362955 c +12.858298 15.364022 l +11.707717 15.822705 9.179629 16.769213 8.027948 17.170942 c +8.018206 17.174341 8.008386 17.177513 7.998495 17.180454 c +7.995981 17.181202 l +7.783853 17.244291 7.495684 17.329998 7.169050 17.329998 c +h +7.165000 12.829998 m +7.532269 12.829998 7.830000 12.532268 7.830000 12.164998 c +7.830000 7.664998 l +7.830000 7.297729 7.532269 6.999998 7.165000 6.999998 c +6.797730 6.999998 6.500000 7.297729 6.500000 7.664998 c +6.500000 12.164998 l +6.500000 12.532268 6.797730 12.829998 7.165000 12.829998 c +h +8.165000 5.164998 m +8.165000 4.612713 7.717285 4.164998 7.165000 4.164998 c +6.612715 4.164998 6.165000 4.612713 6.165000 5.164998 c +6.165000 5.717283 6.612715 6.164998 7.165000 6.164998 c +7.717285 6.164998 8.165000 5.717283 8.165000 5.164998 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 3222 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000003312 00000 n +0000003335 00000 n +0000003508 00000 n +0000003582 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3641 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 56e3d67b97..330977e2e3 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -24,6 +24,8 @@ import UndoUI import TelegramCallsUI import WallpaperBackgroundNode import BotPaymentsUI +import ContextUI +import Pasteboard private final class ChatRecentActionsListOpaqueState { let entries: [ChatRecentActionsEntry] @@ -56,13 +58,14 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private var automaticMediaDownloadSettings: MediaAutoDownloadSettings private var containerLayout: (ContainerViewLayout, CGFloat)? + private var visibleAreaInset = UIEdgeInsets() private let backgroundNode: WallpaperBackgroundNode private let panelBackgroundNode: NavigationBackgroundNode private let panelSeparatorNode: ASDisplayNode private let panelButtonNode: HighlightableButtonNode - private let listNode: ListView + fileprivate let listNode: ListView private let loadingNode: ChatLoadingNode private let emptyNode: ChatRecentActionsEmptyNode @@ -86,6 +89,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private var adminsDisposable: Disposable? private var adminsState: ChannelMemberListState? private let banDisposables = DisposableDict() + private let reportFalsePositiveDisposables = DisposableDict() private weak var antiSpamTooltipController: UndoOverlayController? @@ -258,8 +262,10 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } }, openPeerMention: { [weak self] name in self?.openPeerMention(name) - }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _, location in - self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame, location: location) + }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, anyRecognizer, location in + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = anyRecognizer as? ContextGesture + self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame, recognizer: recognizer, gesture: gesture, location: location) }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in @@ -628,6 +634,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self.resolvePeerByNameDisposable.dispose() self.adminsDisposable?.dispose() self.banDisposables.dispose() + self.reportFalsePositiveDisposables.dispose() } func updatePresentationData(_ presentationData: PresentationData) { @@ -664,6 +671,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { transition.updateFrame(node: self.panelSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.panelButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: intrinsicPanelHeight))) + self.visibleAreaInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: panelHeight, right: 0.0) + transition.updateBounds(node: self.listNode, bounds: CGRect(origin: CGPoint(), size: layout.size)) transition.updatePosition(node: self.listNode, position: CGRect(origin: CGPoint(), size: layout.size).center) @@ -819,15 +828,51 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { })) } - private func openMessageContextMenu(message: Message, selectAll: Bool, node: ASDisplayNode, frame: CGRect, location: CGPoint?) { - var actions: [ContextMenuAction] = [] + private func openMessageContextMenu(message: Message, selectAll: Bool, node: ASDisplayNode, frame: CGRect, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, location: CGPoint? = nil) { + guard let controller = self.controller else { + return + } + self.dismissAllTooltips() + + let context = self.context + let source: ContextContentSource + if let location = location { + source = .location(ChatMessageContextLocationContentSource(controller: controller, location: node.view.convert(node.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) + } else { + source = .extracted(ChatRecentActionsMessageContextExtractedContentSource(controllerNode: self, message: message, selectAll: selectAll)) + } + + var actions: [ContextMenuItem] = [] if !message.text.isEmpty { - actions.append(ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { - UIPasteboard.general.string = message.text - - let content: UndoOverlayContent = .copy(text: self.presentationData.strings.Conversation_TextCopied) - self.presentController(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), .current, nil) - })) + actions.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + if let strongSelf = self { + var messageEntities: [MessageTextEntity]? + var restrictedText: String? + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + messageEntities = attribute.entities + } + if let attribute = attribute as? RestrictedContentMessageAttribute { + restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? "" + } + } + + if let restrictedText = restrictedText { + storeMessageTextInPasteboard(restrictedText, entities: nil) + } else { + storeMessageTextInPasteboard(message.text, entities: messageEntities) + } + + Queue.mainQueue().after(0.2, { + let content: UndoOverlayContent = .copy(text: strongSelf.presentationData.strings.Conversation_TextCopied) + strongSelf.presentController(UndoOverlayController(presentationData: strongSelf.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), .current, nil) + }) + } + })) + ) } if let author = message.author, let adminsState = self.adminsState { @@ -847,42 +892,54 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } if canBan { - actions.append(ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuBan, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuBan), action: { [weak self] in - if let strongSelf = self { - strongSelf.banDisposables.set((strongSelf.context.engine.peers.fetchChannelParticipant(peerId: strongSelf.peer.id, participantId: author.id) - |> deliverOnMainQueue).start(next: { participant in - if let strongSelf = self { - strongSelf.presentController(channelBannedMemberController(context: strongSelf.context, peerId: strongSelf.peer.id, memberId: author.id, initialParticipant: participant, updated: { _ in }, upgradedToSupergroup: { _, f in f() }), .window(.root), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } - }), forKey: author.id) - } - })) + actions.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuBan, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + if let strongSelf = self { + f(.default) + strongSelf.banDisposables.set((strongSelf.context.engine.peers.fetchChannelParticipant(peerId: strongSelf.peer.id, participantId: author.id) + |> deliverOnMainQueue).start(next: { participant in + if let strongSelf = self { + strongSelf.presentController(channelBannedMemberController(context: strongSelf.context, peerId: strongSelf.peer.id, memberId: author.id, initialParticipant: participant, updated: { _ in }, upgradedToSupergroup: { _, f in f() }), .window(.root), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + }), forKey: author.id) + } + })) + ) } } - if !actions.isEmpty { - let contextMenuController = ContextMenuController(actions: actions) - - self.controllerInteraction.highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) - self.updateItemNodesHighlightedStates(animated: true) - - contextMenuController.dismissed = { [weak self] in - if let strongSelf = self { - if strongSelf.controllerInteraction.highlightedState?.messageStableId == message.stableId { - strongSelf.controllerInteraction.highlightedState = nil - strongSelf.updateItemNodesHighlightedStates(animated: true) - } + let configuration = AntiSpamBotConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + for peer in message.peers { + if peer.0 == configuration.antiSpamBotId { + if !actions.isEmpty { + actions.insert(.separator, at: 0) } + actions.insert( + .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuReportFalsePositive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AntiSpam"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + if let strongSelf = self { + strongSelf.reportFalsePositiveDisposables.set((strongSelf.context.engine.peers.reportAntiSpamFalsePositive(peerId: message.id.peerId, messageId: message.id) + |> deliverOnMainQueue).start(), forKey: message.id) + + Queue.mainQueue().after(0.2, { + let content: UndoOverlayContent = .image(image: UIImage(bundleImageName: "Chat/AntiSpamTooltipIcon")!, title: nil, text: strongSelf.presentationData.strings.Group_AdminLog_AntiSpamFalsePositiveReportedText, undo: false) + strongSelf.presentController(UndoOverlayController(presentationData: strongSelf.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), .current, nil) + }) + } + })), at: 0 + ) + + break } - - self.presentController(contextMenuController, .window(.root), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak node] in - if let strongSelf = self, let node = node { - return (node, frame, strongSelf, strongSelf.bounds) - } else { - return nil - } - })) } + + guard !actions.isEmpty else { + return + } + + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(actions))), recognizer: recognizer, gesture: gesture) + controller.window?.presentInGlobalOverlay(contextController) } private func updateItemNodesHighlightedStates(animated: Bool) { @@ -1043,4 +1100,70 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { return true }) } + + func frameForVisibleArea() -> CGRect { + let rect = CGRect(origin: CGPoint(x: self.visibleAreaInset.left, y: self.visibleAreaInset.top), size: CGSize(width: self.bounds.size.width - self.visibleAreaInset.left - self.visibleAreaInset.right, height: self.bounds.size.height - self.visibleAreaInset.top - self.visibleAreaInset.bottom)) + + return rect + } +} + +final class ChatRecentActionsMessageContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = true + + private weak var controllerNode: ChatRecentActionsControllerNode? + private let message: Message + private let selectAll: Bool + + var shouldBeDismissed: Signal { + return .single(false) + } + + init(controllerNode: ChatRecentActionsControllerNode, message: Message, selectAll: Bool) { + self.controllerNode = controllerNode + self.message = message + self.selectAll = selectAll + } + + func takeView() -> ContextControllerTakeViewInfo? { + guard let controllerNode = self.controllerNode else { + return nil + } + + var result: ContextControllerTakeViewInfo? + controllerNode.listNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.content.contains(where: { $0.0.stableId == self.message.stableId }), let contentNode = itemNode.getMessageContextSourceNode(stableId: self.selectAll ? nil : self.message.stableId) { + result = ContextControllerTakeViewInfo(containingItem: .node(contentNode), contentAreaInScreenSpace: controllerNode.convert(controllerNode.frameForVisibleArea(), to: nil)) + } + } + return result + } + + func putBack() -> ContextControllerPutBackViewInfo? { + guard let controllerNode = self.controllerNode else { + return nil + } + + var result: ContextControllerPutBackViewInfo? + controllerNode.listNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.content.contains(where: { $0.0.stableId == self.message.stableId }) { + result = ContextControllerPutBackViewInfo(contentAreaInScreenSpace: controllerNode.convert(controllerNode.frameForVisibleArea(), to: nil)) + } + } + return result + } } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift index f93677edd6..ce5e644bd7 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift @@ -144,7 +144,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { }, to: &text, entities: &entities) } let action = TelegramMediaActionType.customText(text: text, entities: entities) - let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) + let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: let peers = SimpleDictionary() @@ -182,7 +182,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { }, to: &text, entities: &entities) } let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities) - let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) + let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var previousAttributes: [MessageAttribute] = [] @@ -232,7 +232,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { }, to: &text, entities: &entities) } let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities) - let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) + let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var previousAttributes: [MessageAttribute] = [] @@ -374,7 +374,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { }, to: &text, entities: &entities) let action = TelegramMediaActionType.customText(text: text, entities: entities) - let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) + let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: if let message = message { @@ -459,7 +459,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) - let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) + let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() @@ -502,7 +502,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) - let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) + let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() @@ -526,6 +526,9 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } } + if let peer = self.entry.peers[self.entry.event.peerId] { + peers[peer.id] = peer + } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } @@ -1072,7 +1075,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) - let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) + let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() @@ -1634,7 +1637,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { }, to: &text, entities: &entities) let action = TelegramMediaActionType.customText(text: text, entities: entities) - let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) + let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary()