From 54a57205eecbe445c860f1ccdf4f29b4cdeaf3d5 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 19 Feb 2021 20:27:45 +0400 Subject: [PATCH] Add View in Chat for messages in channel stats --- .../Sources/ChannelStatsController.swift | 55 ++++++++++++- .../Sources/StatsMessageItem.swift | 81 +++++++++++++++++-- 2 files changed, 127 insertions(+), 9 deletions(-) diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 3182fb15d4..2bae41250a 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import Display +import AsyncDisplayKit import SwiftSignalKit import Postbox import TelegramCore @@ -14,16 +15,19 @@ import AccountContext import PresentationDataUtils import AppBundle import GraphUI +import ContextUI private final class ChannelStatsControllerArguments { let context: AccountContext let loadDetailedGraph: (StatsGraph, Int64) -> Signal let openMessageStats: (MessageId) -> Void + let contextAction: (MessageId, ASDisplayNode, ContextGesture?) -> Void - init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal, openMessage: @escaping (MessageId) -> Void) { + init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void) { self.context = context self.loadDetailedGraph = loadDetailedGraph self.openMessageStats = openMessage + self.contextAction = contextAction } } @@ -330,6 +334,8 @@ private enum StatsEntry: ItemListNodeEntry { case let .post(_, _, _, _, message, interactions): return StatsMessageItem(context: arguments.context, presentationData: presentationData, message: message, views: interactions.views, forwards: interactions.forwards, sectionId: self.section, style: .blocks, action: { arguments.openMessageStats(message.id) + }, contextAction: { node, gesture in + arguments.contextAction(message.id, node, gesture) }) } } @@ -407,6 +413,7 @@ private func channelStatsControllerEntries(data: ChannelStats?, messages: [Messa public func channelStatsController(context: AccountContext, peerId: PeerId, cachedPeerData: CachedPeerData) -> ViewController { var openMessageStatsImpl: ((MessageId) -> Void)? + var contextActionImpl: ((MessageId, ASDisplayNode, ContextGesture?) -> Void)? let actionsDisposable = DisposableSet() let dataPromise = Promise(nil) @@ -440,6 +447,8 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach return statsContext.loadDetailedGraph(graph, x: x) }, openMessage: { messageId in openMessageStatsImpl?(messageId) + }, contextAction: { messageId, node, gesture in + contextActionImpl?(messageId, node, gesture) }) let messageView = context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId), index: .upperBound, anchorIndex: .upperBound, count: 100, fixedCombinedReadStates: nil) @@ -496,9 +505,49 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach controller?.clearItemNodesHighlight(animated: true) } openMessageStatsImpl = { [weak controller] messageId in - if let navigationController = controller?.navigationController as? NavigationController { - controller?.push(messageStatsController(context: context, messageId: messageId, cachedPeerData: cachedPeerData)) + controller?.push(messageStatsController(context: context, messageId: messageId, cachedPeerData: cachedPeerData)) + } + contextActionImpl = { [weak controller] messageId, sourceNode, gesture in + guard let controller = controller, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else { + return } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak controller] c, _ in + c.dismiss(completion: { + if let navigationController = controller?.navigationController as? NavigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), subject: .message(id: messageId, highlight: true))) + } + }) + }))) + + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(ChannelStatsContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(items), reactionItems: [], gesture: gesture) + controller.presentInGlobalOverlay(contextController) } return controller } + +private final class ChannelStatsContextExtractedContentSource: ContextExtractedContentSource { + var keepInPlace: Bool + let ignoreContentTouches: Bool = true + let blurBackground: Bool = true + + private let controller: ViewController + private let sourceNode: ContextExtractedContentContainingNode + + init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool) { + self.controller = controller + self.sourceNode = sourceNode + self.keepInPlace = keepInPlace + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/StatisticsUI/Sources/StatsMessageItem.swift b/submodules/StatisticsUI/Sources/StatsMessageItem.swift index 84b7b43fc8..0851a6a5e8 100644 --- a/submodules/StatisticsUI/Sources/StatsMessageItem.swift +++ b/submodules/StatisticsUI/Sources/StatsMessageItem.swift @@ -22,8 +22,9 @@ public class StatsMessageItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let style: ItemListStyle let action: (() -> Void)? + let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? - init(context: AccountContext, presentationData: ItemListPresentationData, message: Message, views: Int32, forwards: Int32, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?) { + init(context: AccountContext, presentationData: ItemListPresentationData, message: Message, views: Int32, forwards: Int32, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?) { self.context = context self.presentationData = presentationData self.message = message @@ -32,6 +33,7 @@ public class StatsMessageItem: ListViewItem, ItemListItem { self.sectionId = sectionId self.style = style self.action = action + self.contextAction = contextAction } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -88,6 +90,8 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode { let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode private let extractedBackgroundImageNode: ASImageNode + private let offsetContainerNode: ASDisplayNode + private let countersContainerNode: ASDisplayNode private var extractedRect: CGRect? private var nonExtractedRect: CGRect? @@ -134,6 +138,9 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode { self.contentImageNode = TransformImageNode() self.contentImageNode.isLayerBacked = true + self.offsetContainerNode = ASDisplayNode() + self.countersContainerNode = ASDisplayNode() + self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false @@ -153,13 +160,56 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode { super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.contentImageNode) - self.addSubnode(self.titleNode) - self.addSubnode(self.labelNode) - self.addSubnode(self.viewsNode) - self.addSubnode(self.forwardsNode) + self.containerNode.addSubnode(self.contextSourceNode) + self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode + self.addSubnode(self.containerNode) + + self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode) + self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode) + self.contextSourceNode.contentNode.addSubnode(self.countersContainerNode) + + self.offsetContainerNode.addSubnode(self.contentImageNode) + self.offsetContainerNode.addSubnode(self.titleNode) + self.offsetContainerNode.addSubnode(self.labelNode) + self.countersContainerNode.addSubnode(self.viewsNode) + self.countersContainerNode.addSubnode(self.forwardsNode) + self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode self.addSubnode(self.activateArea) + + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self, let item = strongSelf.item, let contextAction = item.contextAction else { + gesture.cancel() + return + } + contextAction(strongSelf.contextSourceNode, gesture) + } + + self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + + if isExtracted { + strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.list.itemBlocksBackgroundColor) + } + + if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect { + let rect = isExtracted ? extractedRect : nonExtractedRect + transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect) + } + + transition.updateAlpha(node: strongSelf.countersContainerNode, alpha: isExtracted ? 0.0 : 1.0) + + transition.updateSublayerTransformOffset(layer: strongSelf.countersContainerNode.layer, offset: CGPoint(x: isExtracted ? -24.0 : 0.0, y: 0.0)) + transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0)) + + transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in + if !isExtracted { + self?.extractedBackgroundImageNode.image = nil + } + }) + } } public func asyncLayout() -> (_ item: StatsMessageItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { @@ -271,6 +321,25 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode { if let strongSelf = self { strongSelf.item = item + let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width - 16.0, height: layout.contentSize.height)) + let extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0) + strongSelf.extractedRect = extractedRect + strongSelf.nonExtractedRect = nonExtractedRect + + if strongSelf.contextSourceNode.isExtractedToContextPreview { + strongSelf.extractedBackgroundImageNode.frame = extractedRect + } else { + strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect + } + strongSelf.contextSourceNode.contentRect = extractedRect + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.countersContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.containerNode.isGestureEnabled = item.contextAction != nil + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) strongSelf.activateArea.accessibilityLabel = text strongSelf.activateArea.accessibilityValue = label