From 3926531c398987702053d91905cbbc664f740399 Mon Sep 17 00:00:00 2001 From: Peter <> Date: Wed, 7 Aug 2019 04:02:38 +0300 Subject: [PATCH] First context menu implementation --- .../contents.xcworkspacedata | 6 +- .../Telegram-iOS-Hockeyapp-Internal.xcscheme | 1 - .../ContextUI_Xcode.xcodeproj/project.pbxproj | 20 + .../ContextUI/Sources/ContextActionNode.swift | 182 +++++++ .../Sources/ContextActionsContainerNode.swift | 93 ++++ .../Sources/ContextContentContainerNode.swift | 14 + .../Sources/ContextContentSourceNode.swift | 24 + .../ContextUI/Sources/ContextController.swift | 502 +++++++++++++++++- .../GlobalOverlayPresentationContext.swift | 46 +- submodules/Display/Display/ListView.swift | 4 +- .../Display/Display/ListViewItemNode.swift | 4 + submodules/Display/Display/UIKitUtils.swift | 2 + .../Display/Display/WindowContent.swift | 2 +- .../Sources/ChatMessageBubbleImages.swift | 6 +- .../PresentationThemeEssentialGraphics.swift | 36 ++ .../TelegramUI/ChatController.swift | 97 ++-- .../TelegramUI/ChatControllerNode.swift | 9 +- .../ChatInterfaceStateContextMenus.swift | 108 ++-- .../ChatMessageAnimatedStickerItemNode.swift | 23 +- .../TelegramUI/ChatMessageBackground.swift | 83 +-- .../ChatMessageBubbleBackdrop.swift | 93 +++- .../ChatMessageBubbleItemNode.swift | 125 ++++- ...essageContextControllerContentSource.swift | 55 ++ .../ChatMessageInstantVideoItemNode.swift | 19 +- .../TelegramUI/ChatMessageItemView.swift | 5 + .../ChatMessageStickerItemNode.swift | 21 +- .../ChatPanelInterfaceInteraction.swift | 5 +- .../ChatRecentActionsController.swift | 3 +- .../FetchCachedRepresentations.swift | 2 +- .../PeerMediaCollectionController.swift | 3 +- .../project.pbxproj | 12 + 31 files changed, 1414 insertions(+), 191 deletions(-) create mode 100644 submodules/ContextUI/Sources/ContextActionNode.swift create mode 100644 submodules/ContextUI/Sources/ContextActionsContainerNode.swift create mode 100644 submodules/ContextUI/Sources/ContextContentContainerNode.swift create mode 100644 submodules/ContextUI/Sources/ContextContentSourceNode.swift create mode 100644 submodules/TelegramUI/TelegramUI/ChatMessageContextControllerContentSource.swift diff --git a/Telegram-iOS.xcworkspace/contents.xcworkspacedata b/Telegram-iOS.xcworkspace/contents.xcworkspacedata index 51e4105dc2..5315fd3081 100644 --- a/Telegram-iOS.xcworkspace/contents.xcworkspacedata +++ b/Telegram-iOS.xcworkspace/contents.xcworkspacedata @@ -76,6 +76,9 @@ + + @@ -170,9 +173,6 @@ - - diff --git a/Telegram-iOS.xcworkspace/xcshareddata/xcschemes/Telegram-iOS-Hockeyapp-Internal.xcscheme b/Telegram-iOS.xcworkspace/xcshareddata/xcschemes/Telegram-iOS-Hockeyapp-Internal.xcscheme index 61e39f8bfe..7152c964ab 100644 --- a/Telegram-iOS.xcworkspace/xcshareddata/xcschemes/Telegram-iOS-Hockeyapp-Internal.xcscheme +++ b/Telegram-iOS.xcworkspace/xcshareddata/xcschemes/Telegram-iOS-Hockeyapp-Internal.xcscheme @@ -79,7 +79,6 @@ buildConfiguration = "DebugHockeyapp" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - enableAddressSanitizer = "YES" enableASanStackUseAfterReturn = "YES" launchStyle = "0" useCustomWorkingDirectory = "NO" diff --git a/submodules/ContextUI/ContextUI_Xcode.xcodeproj/project.pbxproj b/submodules/ContextUI/ContextUI_Xcode.xcodeproj/project.pbxproj index b95836cf04..fe0404253d 100644 --- a/submodules/ContextUI/ContextUI_Xcode.xcodeproj/project.pbxproj +++ b/submodules/ContextUI/ContextUI_Xcode.xcodeproj/project.pbxproj @@ -13,6 +13,11 @@ D038AC7522F8A06200320981 /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D038AC7422F8A06200320981 /* Display.framework */; }; D038AC7722F8A07000320981 /* ContextController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D038AC7622F8A07000320981 /* ContextController.swift */; }; D038AC7922F8A08A00320981 /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D038AC7822F8A08A00320981 /* AsyncDisplayKit.framework */; }; + D09E777F22F8E47000B9CCA7 /* TelegramPresentationData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E777E22F8E47000B9CCA7 /* TelegramPresentationData.framework */; }; + D09E778122F8E62000B9CCA7 /* ContextActionsContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E778022F8E62000B9CCA7 /* ContextActionsContainerNode.swift */; }; + D09E778322F8E67300B9CCA7 /* ContextActionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E778222F8E67300B9CCA7 /* ContextActionNode.swift */; }; + D09E778522F8E83600B9CCA7 /* ContextContentContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E778422F8E83600B9CCA7 /* ContextContentContainerNode.swift */; }; + D09E778D22FA055100B9CCA7 /* ContextContentSourceNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E778C22FA055100B9CCA7 /* ContextContentSourceNode.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -24,6 +29,11 @@ D038AC7422F8A06200320981 /* Display.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Display.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D038AC7622F8A07000320981 /* ContextController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextController.swift; sourceTree = ""; }; D038AC7822F8A08A00320981 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D09E777E22F8E47000B9CCA7 /* TelegramPresentationData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramPresentationData.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D09E778022F8E62000B9CCA7 /* ContextActionsContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextActionsContainerNode.swift; sourceTree = ""; }; + D09E778222F8E67300B9CCA7 /* ContextActionNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextActionNode.swift; sourceTree = ""; }; + D09E778422F8E83600B9CCA7 /* ContextContentContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextContentContainerNode.swift; sourceTree = ""; }; + D09E778C22FA055100B9CCA7 /* ContextContentSourceNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextContentSourceNode.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -31,6 +41,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D09E777F22F8E47000B9CCA7 /* TelegramPresentationData.framework in Frameworks */, D038AC7922F8A08A00320981 /* AsyncDisplayKit.framework in Frameworks */, D038AC7522F8A06200320981 /* Display.framework in Frameworks */, D038AC7322F8A05F00320981 /* UIKit.framework in Frameworks */, @@ -64,6 +75,10 @@ children = ( D038AC6322F8A00900320981 /* ContextUI.h */, D038AC7622F8A07000320981 /* ContextController.swift */, + D09E778022F8E62000B9CCA7 /* ContextActionsContainerNode.swift */, + D09E778222F8E67300B9CCA7 /* ContextActionNode.swift */, + D09E778422F8E83600B9CCA7 /* ContextContentContainerNode.swift */, + D09E778C22FA055100B9CCA7 /* ContextContentSourceNode.swift */, ); path = Sources; sourceTree = ""; @@ -71,6 +86,7 @@ D038AC6F22F8A05A00320981 /* Frameworks */ = { isa = PBXGroup; children = ( + D09E777E22F8E47000B9CCA7 /* TelegramPresentationData.framework */, D038AC7822F8A08A00320981 /* AsyncDisplayKit.framework */, D038AC7422F8A06200320981 /* Display.framework */, D038AC7222F8A05F00320981 /* UIKit.framework */, @@ -160,6 +176,10 @@ buildActionMask = 2147483647; files = ( D038AC7722F8A07000320981 /* ContextController.swift in Sources */, + D09E778122F8E62000B9CCA7 /* ContextActionsContainerNode.swift in Sources */, + D09E778522F8E83600B9CCA7 /* ContextContentContainerNode.swift in Sources */, + D09E778322F8E67300B9CCA7 /* ContextActionNode.swift in Sources */, + D09E778D22FA055100B9CCA7 /* ContextContentSourceNode.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/submodules/ContextUI/Sources/ContextActionNode.swift b/submodules/ContextUI/Sources/ContextActionNode.swift new file mode 100644 index 0000000000..dd2e6173a1 --- /dev/null +++ b/submodules/ContextUI/Sources/ContextActionNode.swift @@ -0,0 +1,182 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramPresentationData + +private let textFont = Font.regular(17.0) + +enum ContextActionNext { + case none + case item + case separator +} + +final class ContextActionNode: ASDisplayNode { + private let action: ContextMenuActionItem + private let getController: () -> ContextController? + private let actionSelected: (ContextMenuActionResult) -> Void + + private let backgroundNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let separatorNode: ASDisplayNode + private let textNode: ImmediateTextNode + private let statusNode: ImmediateTextNode? + private let iconNode: ASImageNode + private let buttonNode: HighlightTrackingButtonNode + + init(theme: PresentationTheme, action: ContextMenuActionItem, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + self.action = action + self.getController = getController + self.actionSelected = actionSelected + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isAccessibilityElement = false + if theme.chatList.searchBarKeyboardColor == .dark { + self.backgroundNode.backgroundColor = theme.actionSheet.itemBackgroundColor.withAlphaComponent(0.8) + } else { + self.backgroundNode.backgroundColor = UIColor(white: 1.0, alpha: 0.6) + } + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isAccessibilityElement = false + if theme.chatList.searchBarKeyboardColor == .dark { + self.highlightedBackgroundNode.backgroundColor = theme.actionSheet.opaqueItemHighlightedBackgroundColor + } else { + self.highlightedBackgroundNode.backgroundColor = UIColor(white: 0.8, alpha: 0.6) + } + self.highlightedBackgroundNode.alpha = 0.0 + + self.separatorNode = ASDisplayNode() + self.separatorNode.isAccessibilityElement = false + self.separatorNode.backgroundColor = UIColor(white: 0.0, alpha: 0.1) + + self.textNode = ImmediateTextNode() + self.textNode.isAccessibilityElement = false + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + let textColor: UIColor + switch action.textColor { + case .primary: + textColor = theme.actionSheet.primaryTextColor + case .destructive: + textColor = theme.actionSheet.destructiveActionTextColor + } + self.textNode.attributedText = NSAttributedString(string: action.text, font: textFont, textColor: textColor) + + switch action.textLayout { + case .singleLine: + self.textNode.maximumNumberOfLines = 1 + self.statusNode = nil + case .twoLinesMax: + self.textNode.maximumNumberOfLines = 2 + self.statusNode = nil + case let .secondLineWithValue(value): + self.textNode.maximumNumberOfLines = 1 + let statusNode = ImmediateTextNode() + statusNode.isAccessibilityElement = false + statusNode.isUserInteractionEnabled = false + statusNode.displaysAsynchronously = false + statusNode.attributedText = NSAttributedString(string: value, font: textFont, textColor: theme.actionSheet.secondaryTextColor) + statusNode.maximumNumberOfLines = 1 + self.statusNode = statusNode + } + + self.iconNode = ASImageNode() + self.iconNode.isAccessibilityElement = false + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.isUserInteractionEnabled = false + self.iconNode.image = action.icon(theme) + + self.buttonNode = HighlightTrackingButtonNode() + self.buttonNode.isAccessibilityElement = true + self.buttonNode.accessibilityLabel = action.text + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.textNode) + self.statusNode.flatMap(self.addSubnode) + self.addSubnode(self.iconNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.highligthedChanged = { [weak self] highligted in + guard let strongSelf = self else { + return + } + if highligted { + strongSelf.highlightedBackgroundNode.alpha = 1.0 + } else { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + func updateLayout(constrainedWidth: CGFloat, next: ContextActionNext) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { + let sideInset: CGFloat = 16.0 + let verticalInset: CGFloat = 12.0 + + let iconSize = self.iconNode.image.flatMap({ $0.size }) ?? CGSize() + + let standardIconWidth: CGFloat = 28.0 + var rightTextInset: CGFloat = 0.0 + if !iconSize.width.isZero { + rightTextInset = max(iconSize.width, standardIconWidth) + sideInset + } + + let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude)) + let statusSize = self.statusNode?.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude)) ?? CGSize() + + switch next { + case .item: + self.separatorNode.alpha = 1.0 + case .none, .separator: + self.separatorNode.alpha = 0.0 + } + + if !statusSize.width.isZero, let statusNode = self.statusNode { + let verticalSpacing: CGFloat = 2.0 + let combinedTextHeight = textSize.height + verticalSpacing + statusSize.height + return (CGSize(width: max(textSize.width, statusSize.width) + sideInset + rightTextInset, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in + let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0) + transition.updateFrameAdditive(node: self.textNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)) + transition.updateFrameAdditive(node: statusNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin + verticalSpacing + textSize.height), size: textSize)) + + if !iconSize.width.isZero { + transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: size.width - standardIconWidth + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)) + } + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + }) + } else { + return (CGSize(width: textSize.width + sideInset + rightTextInset, height: verticalInset * 2.0 + textSize.height), { size, transition in + let verticalOrigin = floor((size.height - textSize.height) / 2.0) + transition.updateFrameAdditive(node: self.textNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)) + + if !iconSize.width.isZero { + transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: size.width - sideInset - standardIconWidth + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)) + } + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + }) + } + } + + @objc private func buttonPressed() { + guard let controller = self.getController() else { + return + } + self.action.action(controller, { [weak self] result in + self?.actionSelected(result) + }) + } +} diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift new file mode 100644 index 0000000000..b54e239027 --- /dev/null +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -0,0 +1,93 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramPresentationData + +private enum ContextItemNode { + case action(ContextActionNode) + case separator(ASDisplayNode) +} + +final class ContextActionsContainerNode: ASDisplayNode { + private var itemNodes: [ContextItemNode] + + init(theme: PresentationTheme, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + self.itemNodes = items.map { item in + switch item { + case let .action(action): + return .action(ContextActionNode(theme: theme, action: action, getController: getController, actionSelected: actionSelected)) + case .separator: + let separatorNode = ASDisplayNode() + if theme.chatList.searchBarKeyboardColor == .dark { + separatorNode.backgroundColor = theme.actionSheet.opaqueItemHighlightedBackgroundColor.withAlphaComponent(0.8) + } else { + separatorNode.backgroundColor = UIColor(white: 0.8, alpha: 0.6) + } + return .separator(separatorNode) + } + } + + super.init() + + self.clipsToBounds = true + self.cornerRadius = 14.0 + + self.itemNodes.forEach({ itemNode in + switch itemNode { + case let .action(actionNode): + self.addSubnode(actionNode) + case let .separator(separatorNode): + self.addSubnode(separatorNode) + } + }) + } + + func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize { + let minActionsWidth = min(constrainedWidth, max(250.0, floor(constrainedWidth / 3.0))) + let separatorHeight: CGFloat = 8.0 + + var maxWidth: CGFloat = 0.0 + var contentHeight: CGFloat = 0.0 + var heightsAndCompletions: [(CGFloat, (CGSize, ContainedViewLayoutTransition) -> Void)?] = [] + for i in 0 ..< self.itemNodes.count { + switch self.itemNodes[i] { + case let .action(itemNode): + let next: ContextActionNext + if i == self.itemNodes.count - 1 { + next = .none + } else if case .separator = self.itemNodes[i + 1] { + next = .separator + } else { + next = .item + } + let (minSize, complete) = itemNode.updateLayout(constrainedWidth: constrainedWidth, next: next) + maxWidth = max(maxWidth, minSize.width) + heightsAndCompletions.append((minSize.height, complete)) + contentHeight += minSize.height + case .separator: + heightsAndCompletions.append(nil) + contentHeight += separatorHeight + } + } + + maxWidth = max(maxWidth, minActionsWidth) + + var verticalOffset: CGFloat = 0.0 + for i in 0 ..< heightsAndCompletions.count { + switch self.itemNodes[i] { + case let .action(itemNode): + if let (itemHeight, itemCompletion) = heightsAndCompletions[i] { + let itemSize = CGSize(width: maxWidth, height: itemHeight) + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: itemSize)) + itemCompletion(itemSize, transition) + verticalOffset += itemHeight + } + case let .separator(separatorNode): + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: maxWidth, height: separatorHeight))) + verticalOffset += separatorHeight + } + } + + return CGSize(width: maxWidth, height: verticalOffset) + } +} diff --git a/submodules/ContextUI/Sources/ContextContentContainerNode.swift b/submodules/ContextUI/Sources/ContextContentContainerNode.swift new file mode 100644 index 0000000000..014cb82458 --- /dev/null +++ b/submodules/ContextUI/Sources/ContextContentContainerNode.swift @@ -0,0 +1,14 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class ContextContentContainerNode: ASDisplayNode { + var contentNode: ContextContentNode? + + override init() { + super.init() + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + } +} diff --git a/submodules/ContextUI/Sources/ContextContentSourceNode.swift b/submodules/ContextUI/Sources/ContextContentSourceNode.swift new file mode 100644 index 0000000000..8aadea4c1a --- /dev/null +++ b/submodules/ContextUI/Sources/ContextContentSourceNode.swift @@ -0,0 +1,24 @@ +import Foundation +import AsyncDisplayKit +import Display + +public final class ContextContentContainingNode: ASDisplayNode { + public let contentNode: ContextContentNode + public var contentRect: CGRect = CGRect() + public var isExtractedToContextPreview: Bool = false + public var isExtractedToContextPreviewUpdated: ((Bool) -> Void)? + public var updateAbsoluteRect: ((CGRect, CGSize) -> Void)? + public var applyAbsoluteOffset: ((CGFloat, ContainedViewLayoutTransitionCurve, Double) -> Void)? + public var applyAbsoluteOffsetSpring: ((CGFloat, Double, CGFloat) -> Void)? + + public override init() { + self.contentNode = ContextContentNode() + + super.init() + + self.addSubnode(self.contentNode) + } +} + +public final class ContextContentNode: ASDisplayNode { +} diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 4680cf9eca..061093a7d5 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1,6 +1,504 @@ import Foundation +import UIKit import AsyncDisplayKit +import Display +import TelegramPresentationData -private final class ContextControllerNode: ASDisplayNode { - +public enum ContextMenuActionItemTextLayout { + case singleLine + case twoLinesMax + case secondLineWithValue(String) +} + +public enum ContextMenuActionItemTextColor { + case primary + case destructive +} + +public enum ContextMenuActionResult { + case `default` + case dismissWithoutContent +} + +public final class ContextMenuActionItem { + public let text: String + public let textColor: ContextMenuActionItemTextColor + public let textLayout: ContextMenuActionItemTextLayout + public let icon: (PresentationTheme) -> UIImage? + public let action: (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void + + public init(text: String, textColor: ContextMenuActionItemTextColor = .primary, textLayout: ContextMenuActionItemTextLayout = .twoLinesMax, icon: @escaping (PresentationTheme) -> UIImage?, action: @escaping (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void) { + self.text = text + self.textColor = textColor + self.textLayout = textLayout + self.icon = icon + self.action = action + } +} + +public enum ContextMenuItem { + case action(ContextMenuActionItem) + case separator +} + +private final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { + private var theme: PresentationTheme + private var strings: PresentationStrings + private let source: ContextControllerContentSource + private var items: [ContextMenuItem] + private let beginDismiss: (ContextMenuActionResult) -> Void + + private var validLayout: ContainerViewLayout? + + private let effectView: UIVisualEffectView + private let dimNode: ASDisplayNode + + private let clippingNode: ASDisplayNode + private let scrollNode: ASScrollNode + + private var originalProjectedContentViewFrame: (CGRect, CGRect)? + private var contentParentNode: ContextContentContainingNode? + private let contentContainerNode: ContextContentContainerNode + private var actionsContainerNode: ContextActionsContainerNode + + init(controller: ContextController, theme: PresentationTheme, strings: PresentationStrings, source: ContextControllerContentSource, items: [ContextMenuItem], beginDismiss: @escaping (ContextMenuActionResult) -> Void) { + self.theme = theme + self.strings = strings + self.source = source + self.items = items + self.beginDismiss = beginDismiss + + self.effectView = UIVisualEffectView() + if #available(iOS 9.0, *) { + } else { + if theme.chatList.searchBarKeyboardColor == .dark { + self.effectView.effect = UIBlurEffect(style: .dark) + } else { + self.effectView.effect = UIBlurEffect(style: .light) + } + self.effectView.alpha = 0.0 + } + + self.dimNode = ASDisplayNode() + if theme.chatList.searchBarKeyboardColor == .dark { + self.dimNode.backgroundColor = theme.chatList.backgroundColor.withAlphaComponent(0.2) + } else { + self.dimNode.backgroundColor = UIColor(rgb: 0xb8bbc1, alpha: 0.5) + } + self.dimNode.alpha = 0.0 + + self.clippingNode = ASDisplayNode() + self.clippingNode.clipsToBounds = true + + self.scrollNode = ASScrollNode() + self.scrollNode.canCancelAllTouchesInViews = true + self.scrollNode.view.delaysContentTouches = false + self.scrollNode.view.showsVerticalScrollIndicator = false + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + + self.contentContainerNode = ContextContentContainerNode() + + var getController: (() -> ContextController?)? + self.actionsContainerNode = ContextActionsContainerNode(theme: theme, items: items, getController: { + return getController?() + }, actionSelected: { result in + beginDismiss(result) + }) + + super.init() + + self.scrollNode.view.delegate = self + + self.view.addSubview(self.effectView) + self.addSubnode(self.dimNode) + + self.addSubnode(self.clippingNode) + + self.clippingNode.addSubnode(self.scrollNode) + + self.scrollNode.addSubnode(self.actionsContainerNode) + self.scrollNode.addSubnode(self.contentContainerNode) + + getController = { [weak controller] in + return controller + } + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTapped))) + } + + @objc private func dimNodeTapped() { + self.beginDismiss(.default) + } + + func animateIn() { + let takenViewInfo = self.source.takeView() + + if let takenViewInfo = takenViewInfo, let parentSupernode = takenViewInfo.contentContainingNode.supernode { + self.contentParentNode = takenViewInfo.contentContainingNode + self.contentContainerNode.contentNode = takenViewInfo.contentContainingNode.contentNode + self.contentContainerNode.addSubnode(takenViewInfo.contentContainingNode.contentNode) + takenViewInfo.contentContainingNode.isExtractedToContextPreview = true + takenViewInfo.contentContainingNode.isExtractedToContextPreviewUpdated?(true) + + self.originalProjectedContentViewFrame = (parentSupernode.view.convert(takenViewInfo.contentContainingNode.frame, to: self.view), takenViewInfo.contentContainingNode.view.convert(takenViewInfo.contentContainingNode.contentRect, to: self.view)) + + self.clippingNode.layer.animateFrame(from: takenViewInfo.contentAreaInScreenSpace, to: self.clippingNode.frame, duration: 0.18, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + self.clippingNode.layer.animateBoundsOriginYAdditive(from: takenViewInfo.contentAreaInScreenSpace.minY, to: 0.0, duration: 0.18, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + + if let validLayout = self.validLayout { + self.updateLayout(layout: validLayout, transition: .immediate, previousActionsContainerNode: nil) + } + + self.dimNode.alpha = 1.0 + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + UIView.animate(withDuration: 0.25, animations: { + if #available(iOS 9.0, *) { + if self.theme.chatList.searchBarKeyboardColor == .dark { + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + if #available(iOSApplicationExtension 13.0, iOS 13.0, *) { + self.effectView.effect = UIBlurEffect(style: .dark) + } else { + self.effectView.effect = UIBlurEffect(style: .regular) + if self.effectView.subviews.count == 2 { + self.effectView.subviews[1].isHidden = true + } + } + } else { + self.effectView.effect = UIBlurEffect(style: .dark) + } + } else { + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + self.effectView.effect = UIBlurEffect(style: .regular) + } else { + self.effectView.effect = UIBlurEffect(style: .light) + } + } + } else { + self.effectView.alpha = 1.0 + } + }, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + if strongSelf.theme.chatList.searchBarKeyboardColor == .dark { + if #available(iOSApplicationExtension 13.0, iOS 13.0, *) { + } else { + if strongSelf.effectView.subviews.count == 2 { + strongSelf.effectView.subviews[1].isHidden = true + } + } + } + }) + //self.effectView.subviews[1].layer.removeAnimation(forKey: "backgroundColor") + + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + let springDuration: Double = 0.42 + let springDamping: CGFloat = 104.0 + self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame, let contentParentNode = self.contentParentNode { + let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) + + self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + let contentContainerOffset = CGPoint(x: localSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) + self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + contentParentNode.applyAbsoluteOffsetSpring?(-contentContainerOffset.y, springDuration, springDamping) + } + } + + func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) { + self.isUserInteractionEnabled = false + + var completedEffect = false + var completedContentNode = false + var completedActionsNode = false + + let putBackInfo = self.source.putBack() + + if let putBackInfo = putBackInfo, let contentParentNode = self.contentParentNode, let parentSupernode = contentParentNode.supernode { + self.originalProjectedContentViewFrame = (parentSupernode.view.convert(contentParentNode.frame, to: self.view), contentParentNode.view.convert(contentParentNode.contentRect, to: self.view)) + + self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: putBackInfo.contentAreaInScreenSpace, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) + self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) + } + + let contentParentNode = self.contentParentNode + let intermediateCompletion: () -> Void = { [weak contentParentNode] in + if completedEffect && completedContentNode && completedActionsNode { + switch result { + case .default: + if let contentParentNode = contentParentNode { + contentParentNode.addSubnode(contentParentNode.contentNode) + contentParentNode.isExtractedToContextPreview = false + contentParentNode.isExtractedToContextPreviewUpdated?(false) + } + case .dismissWithoutContent: + break + } + + completion() + } + } + + UIView.animate(withDuration: 0.3, animations: { + if #available(iOS 9.0, *) { + self.effectView.effect = nil + } else { + self.effectView.alpha = 0.0 + } + }, completion: { _ in + completedEffect = true + intermediateCompletion() + }) + + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + completedActionsNode = true + intermediateCompletion() + }) + self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, removeOnCompletion: false) + if case .default = result, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame, let contentParentNode = self.contentParentNode { + let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) + self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: 0.3, removeOnCompletion: false, additive: true) + let contentContainerOffset = CGPoint(x: localSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) + self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentContainerOffset, duration: 0.3, removeOnCompletion: false, additive: true, completion: { _ in + completedContentNode = true + intermediateCompletion() + }) + contentParentNode.updateAbsoluteRect?(self.contentContainerNode.frame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y + contentContainerOffset.y), self.bounds.size) + contentParentNode.applyAbsoluteOffset?(-contentContainerOffset.y, .easeInOut, 0.3) + } else if let contentParentNode = self.contentParentNode { + if let snapshotView = contentParentNode.contentNode.view.snapshotContentTree() { + self.contentContainerNode.view.addSubview(snapshotView) + } + + contentParentNode.addSubnode(contentParentNode.contentNode) + contentParentNode.isExtractedToContextPreview = false + contentParentNode.isExtractedToContextPreviewUpdated?(false) + + self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + completedContentNode = true + intermediateCompletion() + }) + } + } + + func setItems(controller: ContextController, items: [ContextMenuItem]) { + self.items = items + + let previousActionsContainerNode = self.actionsContainerNode + self.actionsContainerNode = ContextActionsContainerNode(theme: self.theme, items: items, getController: { [weak controller] in + return controller + }, actionSelected: { [weak self] result in + self?.beginDismiss(result) + }) + self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode) + + if let layout = self.validLayout { + self.updateLayout(layout: layout, transition: .animated(duration: 0.3, curve: .spring), previousActionsContainerNode: previousActionsContainerNode) + + } else { + previousActionsContainerNode.removeFromSupernode() + } + } + + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?) { + self.validLayout = layout + + var actionsContainerTransition = transition + if previousActionsContainerNode != nil { + actionsContainerTransition = .immediate + } + + transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let contentActionsSpacing: CGFloat = 11.0 + let actionsSideInset: CGFloat = 11.0 + let contentTopInset: CGFloat = max(11.0, layout.statusBarHeight ?? 0.0) + let actionsBottomInset: CGFloat = 11.0 + + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame, let contentParentNode = self.contentParentNode { + let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero + let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + + let actionsSize = self.actionsContainerNode.updateLayout(constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: actionsContainerTransition) + let contentSize = originalProjectedContentViewFrame.1.size + self.contentContainerNode.updateLayout(size: contentSize, transition: transition) + + let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height) + let originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - actionsSize.width - actionsSideInset, originalProjectedContentViewFrame.1.minX)), y: min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin)), size: actionsSize) + let originalContentFrame = CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalActionsFrame.minY - contentActionsSpacing - originalProjectedContentViewFrame.1.size.height), size: originalProjectedContentViewFrame.1.size) + + let contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) + + let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) + let initialContentOffset = self.scrollNode.view.contentOffset + if self.scrollNode.view.contentSize != scrollContentSize { + self.scrollNode.view.contentSize = scrollContentSize + } + + let overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) + + let contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -overflowOffset - contentParentNode.contentRect.minY) + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) + + if isInitialLayout { + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) + let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + if overflowOffset < 0.0 { + transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) + } + } + + contentParentNode.updateAbsoluteRect?(contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y), layout.size) + } + + if let previousActionsContainerNode = previousActionsContainerNode { + if transition.isAnimated { + transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.1) + previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in + previousActionsContainerNode?.removeFromSupernode() + }) + + transition.animateTransformScale(node: self.actionsContainerNode, from: 0.1) + if transition.isAnimated { + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } else { + previousActionsContainerNode.removeFromSupernode() + } + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if let contentParentNode = self.contentParentNode, let layout = self.validLayout { + let contentContainerFrame = self.contentContainerNode.frame + contentParentNode.updateAbsoluteRect?(contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y), layout.size) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + let mappedPoint = self.view.convert(point, to: self.scrollNode.view) + if self.contentContainerNode.frame.contains(mappedPoint), let contentParentNode = self.contentParentNode, contentParentNode.contentRect.contains(mappedPoint) { + return self.contentContainerNode.hitTest(mappedPoint, with: event) + } + if self.actionsContainerNode.frame.contains(mappedPoint) { + return self.actionsContainerNode.hitTest(self.view.convert(point, to: self.actionsContainerNode.view), with: event) + } + + return self.dimNode.view + } +} + +public final class ContextControllerTakeViewInfo { + public let contentContainingNode: ContextContentContainingNode + public let contentAreaInScreenSpace: CGRect + + public init(contentContainingNode: ContextContentContainingNode, contentAreaInScreenSpace: CGRect) { + self.contentContainingNode = contentContainingNode + self.contentAreaInScreenSpace = contentAreaInScreenSpace + } +} + +public final class ContextControllerPutBackViewInfo { + public let contentAreaInScreenSpace: CGRect + + public init(contentAreaInScreenSpace: CGRect) { + self.contentAreaInScreenSpace = contentAreaInScreenSpace + } +} + +public protocol ContextControllerContentSource: class { + func takeView() -> ContextControllerTakeViewInfo? + func putBack() -> ContextControllerPutBackViewInfo? +} + +public final class ContextController: ViewController { + private var theme: PresentationTheme + private var strings: PresentationStrings + private let source: ContextControllerContentSource + private var items: [ContextMenuItem] + + private var animatedDidAppear = false + private var wasDismissed = false + + private let hapticFeedback = HapticFeedback() + + private var controllerNode: ContextControllerNode { + return self.displayNode as! ContextControllerNode + } + + public init(theme: PresentationTheme, strings: PresentationStrings, source: ContextControllerContentSource, items: [ContextMenuItem]) { + self.theme = theme + self.strings = strings + self.source = source + self.items = items + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = ContextControllerNode(controller: self, theme: self.theme, strings: self.strings, source: self.source, items: self.items, beginDismiss: { [weak self] result in + self?.dismiss(result: result) + }) + + self.displayNodeDidLoad() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.updateLayout(layout: layout, transition: transition, previousActionsContainerNode: nil) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.wasDismissed && !self.animatedDidAppear { + self.animatedDidAppear = true + self.controllerNode.animateIn() + self.hapticFeedback.impact() + } + } + + public func setItems(_ items: [ContextMenuItem]) { + self.items = items + if self.isNodeLoaded { + self.controllerNode.setItems(controller: self, items: items) + } + } + + private func dismiss(result: ContextMenuActionResult) { + if !self.wasDismissed { + self.wasDismissed = true + self.controllerNode.animateOut(result: result, completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + }) + } + } + + public func dismiss() { + self.dismiss(result: .default) + } } diff --git a/submodules/Display/Display/GlobalOverlayPresentationContext.swift b/submodules/Display/Display/GlobalOverlayPresentationContext.swift index 7da0302467..032b8d5c77 100644 --- a/submodules/Display/Display/GlobalOverlayPresentationContext.swift +++ b/submodules/Display/Display/GlobalOverlayPresentationContext.swift @@ -25,6 +25,7 @@ private func isViewVisibleInHierarchy(_ view: UIView, _ initial: Bool = true) -> final class GlobalOverlayPresentationContext { private let statusBarHost: StatusBarHost? + private weak var parentView: UIView? private var controllers: [ContainableController] = [] @@ -32,19 +33,24 @@ final class GlobalOverlayPresentationContext { private var layout: ContainerViewLayout? private var ready: Bool { - return self.currentPresentationView() != nil && self.layout != nil + return self.currentPresentationView(underStatusBar: false) != nil && self.layout != nil } - init(statusBarHost: StatusBarHost?) { + init(statusBarHost: StatusBarHost?, parentView: UIView) { self.statusBarHost = statusBarHost + self.parentView = parentView } - private func currentPresentationView() -> UIView? { + private func currentPresentationView(underStatusBar: Bool) -> UIView? { if let statusBarHost = self.statusBarHost { if let keyboardWindow = statusBarHost.keyboardWindow, let keyboardView = statusBarHost.keyboardView, !keyboardView.frame.height.isZero, isViewVisibleInHierarchy(keyboardView) { return keyboardWindow } else { - return statusBarHost.statusBarWindow + if underStatusBar, let view = self.parentView { + return view + } else { + return statusBarHost.statusBarWindow + } } } return nil @@ -57,7 +63,13 @@ final class GlobalOverlayPresentationContext { |> deliverOnMainQueue |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(true)) - if let _ = self.currentPresentationView(), let initialLayout = self.layout { + var underStatusBar = false + if let controller = controller as? ViewController { + if case .Ignore = controller.statusBar.statusBarStyle { + underStatusBar = true + } + } + if let _ = self.currentPresentationView(underStatusBar: underStatusBar), let initialLayout = self.layout { controller.view.frame = CGRect(origin: CGPoint(), size: initialLayout.size) controller.containerLayoutUpdated(initialLayout, transition: .immediate) @@ -68,7 +80,7 @@ final class GlobalOverlayPresentationContext { } strongSelf.controllers.append(controller) - if let view = strongSelf.currentPresentationView(), let layout = strongSelf.layout { + if let view = strongSelf.currentPresentationView(underStatusBar: underStatusBar), let layout = strongSelf.layout { (controller as? UIViewController)?.navigation_setDismiss({ [weak controller] in if let strongSelf = self, let controller = controller { strongSelf.dismiss(controller) @@ -99,7 +111,7 @@ final class GlobalOverlayPresentationContext { } private func dismiss(_ controller: ContainableController) { - if let index = self.controllers.index(where: { $0 === controller }) { + if let index = self.controllers.firstIndex(where: { $0 === controller }) { self.controllers.remove(at: index) controller.viewWillDisappear(false) controller.view.removeFromSuperview() @@ -129,13 +141,21 @@ final class GlobalOverlayPresentationContext { } private func addViews() { - if let view = self.currentPresentationView(), let layout = self.layout { + if let layout = self.layout { for controller in self.controllers { - controller.viewWillAppear(false) - view.addSubview(controller.view) - controller.view.frame = CGRect(origin: CGPoint(), size: layout.size) - controller.containerLayoutUpdated(layout, transition: .immediate) - controller.viewDidAppear(false) + var underStatusBar = false + if let controller = controller as? ViewController { + if case .Ignore = controller.statusBar.statusBarStyle { + underStatusBar = true + } + } + if let view = self.currentPresentationView(underStatusBar: underStatusBar) { + controller.viewWillAppear(false) + view.addSubview(controller.view) + controller.view.frame = CGRect(origin: CGPoint(), size: layout.size) + controller.containerLayoutUpdated(layout, transition: .immediate) + controller.viewDidAppear(false) + } } } } diff --git a/submodules/Display/Display/ListView.swift b/submodules/Display/Display/ListView.swift index 68364a7b6b..bb91a43c00 100644 --- a/submodules/Display/Display/ListView.swift +++ b/submodules/Display/Display/ListView.swift @@ -3077,7 +3077,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture nextAccessoryItemNode.transitionOffset = CGPoint() nextAccessoryItemNode.removeFromSupernode() - itemNode.addSubnode(nextAccessoryItemNode) + itemNode.addAccessoryItemNode(nextAccessoryItemNode) itemNode.setAccessoryItemNode(nextAccessoryItemNode, leftInset: leftInset, rightInset: rightInset) self.itemNodes[i].setAccessoryItemNode(nil, leftInset: leftInset, rightInset: rightInset) @@ -3101,7 +3101,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if !didStealAccessoryNode { let accessoryNode = accessoryItem.node(synchronous: synchronous) - itemNode.addSubnode(accessoryNode) + itemNode.addAccessoryItemNode(accessoryNode) itemNode.setAccessoryItemNode(accessoryNode, leftInset: leftInset, rightInset: rightInset) } } diff --git a/submodules/Display/Display/ListViewItemNode.swift b/submodules/Display/Display/ListViewItemNode.swift index af74876d61..a19c7370b7 100644 --- a/submodules/Display/Display/ListViewItemNode.swift +++ b/submodules/Display/Display/ListViewItemNode.swift @@ -100,6 +100,10 @@ open class ListViewItemNode: ASDisplayNode { } } + open func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { + self.addSubnode(accessoryItemNode) + } + final var headerAccessoryItemNode: ListViewAccessoryItemNode? { didSet { if let headerAccessoryItemNode = self.headerAccessoryItemNode { diff --git a/submodules/Display/Display/UIKitUtils.swift b/submodules/Display/Display/UIKitUtils.swift index 3a35f30661..d9029c5ac8 100644 --- a/submodules/Display/Display/UIKitUtils.swift +++ b/submodules/Display/Display/UIKitUtils.swift @@ -339,6 +339,7 @@ public extension UIView { } if let snapshot = snapshot { snapshot.frame = self.frame + snapshot.bounds = self.bounds return snapshot } @@ -358,6 +359,7 @@ public extension CALayer { } if let snapshot = snapshot { snapshot.frame = self.frame + snapshot.bounds = self.bounds return snapshot } diff --git a/submodules/Display/Display/WindowContent.swift b/submodules/Display/Display/WindowContent.swift index 3adce10dbf..dd41b7ba0d 100644 --- a/submodules/Display/Display/WindowContent.swift +++ b/submodules/Display/Display/WindowContent.swift @@ -372,7 +372,7 @@ public class Window1 { self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(boundsSize), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsetsForScreenSize(boundsSize, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil, inVoiceOver: UIAccessibility.isVoiceOverRunning) self.updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate) self.presentationContext = PresentationContext() - self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost) + self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost, parentView: self.hostView.containerView) self.presentationContext.updateIsInteractionBlocked = { [weak self] value in self?.isInteractionBlocked = value diff --git a/submodules/TelegramPresentationData/Sources/ChatMessageBubbleImages.swift b/submodules/TelegramPresentationData/Sources/ChatMessageBubbleImages.swift index e3cab97063..b22f833df9 100644 --- a/submodules/TelegramPresentationData/Sources/ChatMessageBubbleImages.swift +++ b/submodules/TelegramPresentationData/Sources/ChatMessageBubbleImages.swift @@ -26,14 +26,16 @@ public func messageSingleBubbleLikeImage(fillColor: UIColor, strokeColor: UIColo })!.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) } -public func messageBubbleImage(incoming: Bool, fillColor: UIColor, strokeColor: UIColor, neighbors: MessageBubbleImageNeighbors, theme: PresentationThemeChat, wallpaper: TelegramWallpaper, knockout: Bool) -> UIImage { +public func messageBubbleImage(incoming: Bool, fillColor: UIColor, strokeColor: UIColor, neighbors: MessageBubbleImageNeighbors, theme: PresentationThemeChat, wallpaper: TelegramWallpaper, knockout knockoutValue: Bool, mask: Bool = false) -> UIImage { let diameter: CGFloat = 36.0 let corner: CGFloat = 7.0 + let knockout = knockoutValue && !mask + return generateImage(CGSize(width: 42.0, height: diameter), contextGenerator: { size, context in var drawWithClearColor = false if knockout, case let .color(color) = wallpaper { - drawWithClearColor = true + drawWithClearColor = !mask context.setFillColor(UIColor(rgb: UInt32(color)).cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) } else { diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index 083b4c66a5..2783aeda28 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -71,29 +71,41 @@ private func chatBubbleActionButtonImage(fillColor: UIColor, strokeColor: UIColo } public final class PrincipalThemeEssentialGraphics { + public let chatMessageBackgroundIncomingMaskImage: UIImage public let chatMessageBackgroundIncomingImage: UIImage public let chatMessageBackgroundIncomingHighlightedImage: UIImage + public let chatMessageBackgroundIncomingMergedTopMaskImage: UIImage public let chatMessageBackgroundIncomingMergedTopImage: UIImage public let chatMessageBackgroundIncomingMergedTopHighlightedImage: UIImage + public let chatMessageBackgroundIncomingMergedTopSideMaskImage: UIImage public let chatMessageBackgroundIncomingMergedTopSideImage: UIImage public let chatMessageBackgroundIncomingMergedTopSideHighlightedImage: UIImage + public let chatMessageBackgroundIncomingMergedBottomMaskImage: UIImage public let chatMessageBackgroundIncomingMergedBottomImage: UIImage public let chatMessageBackgroundIncomingMergedBottomHighlightedImage: UIImage + public let chatMessageBackgroundIncomingMergedBothMaskImage: UIImage public let chatMessageBackgroundIncomingMergedBothImage: UIImage public let chatMessageBackgroundIncomingMergedBothHighlightedImage: UIImage + public let chatMessageBackgroundIncomingMergedSideMaskImage: UIImage public let chatMessageBackgroundIncomingMergedSideImage: UIImage public let chatMessageBackgroundIncomingMergedSideHighlightedImage: UIImage + public let chatMessageBackgroundOutgoingMaskImage: UIImage public let chatMessageBackgroundOutgoingImage: UIImage public let chatMessageBackgroundOutgoingHighlightedImage: UIImage + public let chatMessageBackgroundOutgoingMergedTopMaskImage: UIImage public let chatMessageBackgroundOutgoingMergedTopImage: UIImage public let chatMessageBackgroundOutgoingMergedTopHighlightedImage: UIImage + public let chatMessageBackgroundOutgoingMergedTopSideMaskImage: UIImage public let chatMessageBackgroundOutgoingMergedTopSideImage: UIImage public let chatMessageBackgroundOutgoingMergedTopSideHighlightedImage: UIImage + public let chatMessageBackgroundOutgoingMergedBottomMaskImage: UIImage public let chatMessageBackgroundOutgoingMergedBottomImage: UIImage public let chatMessageBackgroundOutgoingMergedBottomHighlightedImage: UIImage + public let chatMessageBackgroundOutgoingMergedBothMaskImage: UIImage public let chatMessageBackgroundOutgoingMergedBothImage: UIImage public let chatMessageBackgroundOutgoingMergedBothHighlightedImage: UIImage + public let chatMessageBackgroundOutgoingMergedSideMaskImage: UIImage public let chatMessageBackgroundOutgoingMergedSideImage: UIImage public let chatMessageBackgroundOutgoingMergedSideHighlightedImage: UIImage @@ -201,28 +213,40 @@ public final class PrincipalThemeEssentialGraphics { let emptyImage = UIImage() if preview { + self.chatMessageBackgroundIncomingMaskImage = messageBubbleImage(incoming: true, fillColor: UIColor.black, strokeColor: UIColor.clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundIncomingImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) self.chatMessageBackgroundIncomingHighlightedImage = emptyImage + self.chatMessageBackgroundIncomingMergedTopMaskImage = emptyImage self.chatMessageBackgroundIncomingMergedTopImage = emptyImage self.chatMessageBackgroundIncomingMergedTopHighlightedImage = emptyImage + self.chatMessageBackgroundIncomingMergedTopSideMaskImage = emptyImage self.chatMessageBackgroundIncomingMergedTopSideImage = emptyImage self.chatMessageBackgroundIncomingMergedTopSideHighlightedImage = emptyImage + self.chatMessageBackgroundIncomingMergedBottomMaskImage = emptyImage self.chatMessageBackgroundIncomingMergedBottomImage = emptyImage self.chatMessageBackgroundIncomingMergedBottomHighlightedImage = emptyImage + self.chatMessageBackgroundIncomingMergedBothMaskImage = emptyImage self.chatMessageBackgroundIncomingMergedBothImage = emptyImage self.chatMessageBackgroundIncomingMergedBothHighlightedImage = emptyImage + self.chatMessageBackgroundIncomingMergedSideMaskImage = emptyImage self.chatMessageBackgroundIncomingMergedSideImage = emptyImage self.chatMessageBackgroundIncomingMergedSideHighlightedImage = emptyImage + self.chatMessageBackgroundOutgoingMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundOutgoingImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) self.chatMessageBackgroundOutgoingHighlightedImage = emptyImage + self.chatMessageBackgroundOutgoingMergedTopMaskImage = emptyImage self.chatMessageBackgroundOutgoingMergedTopImage = emptyImage self.chatMessageBackgroundOutgoingMergedTopHighlightedImage = emptyImage + self.chatMessageBackgroundOutgoingMergedTopSideMaskImage = emptyImage self.chatMessageBackgroundOutgoingMergedTopSideImage = emptyImage self.chatMessageBackgroundOutgoingMergedTopSideHighlightedImage = emptyImage + self.chatMessageBackgroundOutgoingMergedBottomMaskImage = emptyImage self.chatMessageBackgroundOutgoingMergedBottomImage = emptyImage self.chatMessageBackgroundOutgoingMergedBottomHighlightedImage = emptyImage + self.chatMessageBackgroundOutgoingMergedBothMaskImage = emptyImage self.chatMessageBackgroundOutgoingMergedBothImage = emptyImage self.chatMessageBackgroundOutgoingMergedBothHighlightedImage = emptyImage + self.chatMessageBackgroundOutgoingMergedSideMaskImage = emptyImage self.chatMessageBackgroundOutgoingMergedSideImage = emptyImage self.chatMessageBackgroundOutgoingMergedSideHighlightedImage = emptyImage self.checkBubbleFullImage = emptyImage @@ -250,29 +274,41 @@ public final class PrincipalThemeEssentialGraphics { self.radialIndicatorFileIconIncoming = emptyImage self.radialIndicatorFileIconOutgoing = emptyImage } else { + self.chatMessageBackgroundIncomingMaskImage = messageBubbleImage(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundIncomingImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) self.chatMessageBackgroundIncomingHighlightedImage = messageBubbleImage(incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) + self.chatMessageBackgroundIncomingMergedTopMaskImage = messageBubbleImage(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .top(side: false), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundIncomingMergedTopImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) self.chatMessageBackgroundIncomingMergedTopHighlightedImage = messageBubbleImage(incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) + self.chatMessageBackgroundIncomingMergedTopSideMaskImage = messageBubbleImage(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .top(side: true), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundIncomingMergedTopSideImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) self.chatMessageBackgroundIncomingMergedTopSideHighlightedImage = messageBubbleImage(incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) + self.chatMessageBackgroundIncomingMergedBottomMaskImage = messageBubbleImage(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .bottom, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundIncomingMergedBottomImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) self.chatMessageBackgroundIncomingMergedBottomHighlightedImage = messageBubbleImage(incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) + self.chatMessageBackgroundIncomingMergedBothMaskImage = messageBubbleImage(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .both, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundIncomingMergedBothImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) self.chatMessageBackgroundIncomingMergedBothHighlightedImage = messageBubbleImage(incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) + self.chatMessageBackgroundOutgoingMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundOutgoingImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) self.chatMessageBackgroundOutgoingHighlightedImage = messageBubbleImage(incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) + self.chatMessageBackgroundOutgoingMergedTopMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .top(side: false), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundOutgoingMergedTopImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) self.chatMessageBackgroundOutgoingMergedTopHighlightedImage = messageBubbleImage(incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) + self.chatMessageBackgroundOutgoingMergedTopSideMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .top(side: true), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundOutgoingMergedTopSideImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) self.chatMessageBackgroundOutgoingMergedTopSideHighlightedImage = messageBubbleImage(incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) + self.chatMessageBackgroundOutgoingMergedBottomMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .white, neighbors: .bottom, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundOutgoingMergedBottomImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) self.chatMessageBackgroundOutgoingMergedBottomHighlightedImage = messageBubbleImage(incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) + self.chatMessageBackgroundOutgoingMergedBothMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .both, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundOutgoingMergedBothImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) self.chatMessageBackgroundOutgoingMergedBothHighlightedImage = messageBubbleImage(incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) + self.chatMessageBackgroundIncomingMergedSideMaskImage = messageBubbleImage(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .side, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundIncomingMergedSideImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) + self.chatMessageBackgroundOutgoingMergedSideMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .side, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) self.chatMessageBackgroundOutgoingMergedSideImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) self.chatMessageBackgroundIncomingMergedSideHighlightedImage = messageBubbleImage(incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) self.chatMessageBackgroundOutgoingMergedSideHighlightedImage = messageBubbleImage(incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index 9a3585c9e1..532e25a1f4 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -20,6 +20,7 @@ import OverlayStatusController import DeviceLocationManager import ShareController import UrlEscaping +import ContextUI public enum ChatControllerPeekActions { case standard @@ -501,51 +502,7 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar guard let strongSelf = self, !actions.isEmpty else { return } - var contextMenuController: ContextMenuController? - var contextActions: [ContextMenuAction] = [] - var sheetActions: [ChatMessageContextMenuSheetAction] = [] - for action in actions { - switch action { - case let .context(contextAction): - contextActions.append(contextAction) - case let .sheet(sheetAction): - sheetActions.append(sheetAction) - } - } - - var hasActions = false - for media in updatedMessages[0].media { - if media is TelegramMediaAction || media is TelegramMediaExpiredContent { - if let action = media as? TelegramMediaAction, case .phoneCall = action.action { - } else { - hasActions = true - } - break - } - } - - if !contextActions.isEmpty { - contextMenuController = ContextMenuController(actions: contextActions, catchTapsOutside: true, hasHapticFeedback: hasActions) - } - - contextMenuController?.dismissed = { - if let strongSelf = self { - strongSelf.chatDisplayNode.displayMessageActionSheet(stableId: nil, sheetActions: nil, displayContextMenuController: nil) - } - } - - if hasActions { - if let contextMenuController = contextMenuController { - strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { - guard let strongSelf = self else { - return nil - } - return (node, frame, strongSelf.displayNode, strongSelf.displayNode.bounds) - })) - } - } else { - strongSelf.chatDisplayNode.displayMessageActionSheet(stableId: updatedMessages[0].stableId, sheetActions: sheetActions, displayContextMenuController: contextMenuController.flatMap { ($0, node, frame) }) - } + strongSelf.window?.presentInGlobalOverlay(ContextController(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: ChatMessageContextControllerContentSource(chatNode: strongSelf.chatDisplayNode, message: message), items: actions)) }) } }, navigateToMessage: { [weak self] fromId, id in @@ -2651,7 +2608,7 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar if let banAuthor = actions.banAuthor { strongSelf.presentBanMessageOptions(accountPeerId: strongSelf.context.account.peerId, author: banAuthor, messageIds: messageIds, options: actions.options) } else { - strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: actions.options) + strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: actions.options, contextController: nil, completion: { _ in }) } } })) @@ -2670,7 +2627,7 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar self?.present(c, in: .window(.root), with: a) }), in: .window(.root)) } - }, deleteMessages: { [weak self] messages in + }, deleteMessages: { [weak self] messages, contextController, completion in if let strongSelf = self, !messages.isEmpty { let messageIds = Set(messages.map { $0.id }) strongSelf.messageContextDisposable.set((chatAvailableMessageActions(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds) @@ -2678,6 +2635,7 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar if let strongSelf = self, !actions.options.isEmpty { if let banAuthor = actions.banAuthor { strongSelf.presentBanMessageOptions(accountPeerId: strongSelf.context.account.peerId, author: banAuthor, messageIds: messageIds, options: actions.options) + completion(.default) } else { var isAction = false if messages.count == 1 { @@ -2689,10 +2647,12 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar } if isAction && (actions.options == .deleteGlobally || actions.options == .deleteLocally) { let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: actions.options == .deleteLocally ? .forLocalPeer : .forEveryone).start() + completion(.dismissWithoutContent) } else if (messages.first?.flags.isSending ?? false) { let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: .forEveryone, deleteAllInGroup: true).start() + completion(.dismissWithoutContent) } else { - strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: actions.options) + strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: actions.options, contextController: contextController, completion: completion) } } } @@ -6770,7 +6730,7 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar } } - private func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions) { + private func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextController?, completion: @escaping (ContextMenuActionResult) -> Void) { let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) var items: [ActionSheetItem] = [] var personalPeerName: String? @@ -6792,8 +6752,13 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar } })) } + + 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 ?? "").0)) items.append(ActionSheetSwitchItem(title: self.presentationData.strings.Chat_UnsendMyMessages, isOn: false, action: { value in unsendPersonalMessages = value @@ -6807,6 +6772,13 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar } else { globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone } + contextItems.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { [weak self] _, f in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: .forEveryone).start() + f(.dismissWithoutContent) + } + }))) items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { @@ -6826,6 +6798,13 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar localOptionText = self.presentationData.strings.Conversation_DeleteManyMessages } } + contextItems.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { [weak self] _, f in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).start() + f(.dismissWithoutContent) + } + }))) items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { @@ -6834,13 +6813,19 @@ public final class ChatController: TelegramBaseController, GalleryHiddenMediaTar } })) } - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - self.chatDisplayNode.dismissInput() - self.present(actionSheet, in: .window(.root)) + + if canDisplayContextMenu, let contextController = contextController { + contextController.setItems(contextItems) + } else { + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.chatDisplayNode.dismissInput() + self.present(actionSheet, in: .window(.root)) + completion(.default) + } } @available(iOSApplicationExtension 11.0, iOS 11.0, *) diff --git a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift index 42728f2785..fa5a86cfb3 100644 --- a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift @@ -82,6 +82,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var restrictedNode: ChatRecentActionsEmptyNode? private var validLayout: (ContainerViewLayout, CGFloat)? + private var visibleAreaInset = UIEdgeInsets() private var searchNavigationNode: ChatSearchNavigationContentNode? @@ -849,7 +850,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - self.loadingNode.updateLayout(size: contentBounds.size, insets: UIEdgeInsets(top: containerInsets.top, left: 0.0, bottom: containerInsets.bottom + contentBottomInset, right: 0.0), transition: transition) + let visibleAreaInset = UIEdgeInsets(top: containerInsets.top, left: 0.0, bottom: containerInsets.bottom + contentBottomInset, right: 0.0) + self.visibleAreaInset = visibleAreaInset + self.loadingNode.updateLayout(size: contentBounds.size, insets: visibleAreaInset, transition: transition) if let containerNode = self.containerNode { contentBottomInset += 8.0 @@ -1551,6 +1554,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { return self.textInputPanelNode?.textInputNode } + func frameForVisibleArea() -> CGRect { + return 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)) + } + func frameForInputPanelAccessoryButton(_ item: ChatTextInputAccessoryItem) -> CGRect? { if let textInputPanelNode = self.textInputPanelNode, self.inputPanelNode === textInputPanelNode { return textInputPanelNode.frameForAccessoryButton(item).flatMap { diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextMenus.swift index 07d56d9cae..156639a0be 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -9,6 +9,7 @@ import MobileCoreServices import TelegramVoip import OverlayStatusController import AccountContext +import ContextUI private struct MessageContextMenuData { let starStatus: Bool? @@ -223,7 +224,7 @@ func updatedChatEditInterfaceMessagetState(state: ChatPresentationInterfaceState return updated } -func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, messages: [Message], controllerInteraction: ChatControllerInteraction?, selectAll: Bool, interfaceInteraction: ChatPanelInterfaceInteraction?) -> Signal<[ChatMessageContextMenuAction], NoError> { +func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, messages: [Message], controllerInteraction: ChatControllerInteraction?, selectAll: Bool, interfaceInteraction: ChatPanelInterfaceInteraction?) -> Signal<[ContextMenuItem], NoError> { guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else { return .single([]) } @@ -348,24 +349,31 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: return MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions) } - return dataSignal |> deliverOnMainQueue |> map { data -> [ChatMessageContextMenuAction] in - var actions: [ChatMessageContextMenuAction] = [] + return dataSignal + |> deliverOnMainQueue + |> map { data -> [ContextMenuItem] in + var actions: [ContextMenuItem] = [] if let starStatus = data.starStatus, let image = starStatus ? starIconFilled : starIconEmpty { - actions.append(.context(ContextMenuAction(content: .icon(image), action: { + actions.append(.action(ContextMenuActionItem(text: starStatus ? "Star" : "Unstar", icon: { theme in + return generateTintedImage(image: image, color: theme.actionSheet.primaryTextColor) + }, action: { _, f in interfaceInteraction.toggleMessageStickerStarred(messages[0].id) + f(.default) }))) } if data.canReply { - actions.append(.context(ContextMenuAction(content: .text(title: chatPresentationInterfaceState.strings.Conversation_ContextMenuReply, accessibilityLabel: chatPresentationInterfaceState.strings.Conversation_ContextMenuReply), action: { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuReply, icon: { _ in nil }, action: { _, f in interfaceInteraction.setupReplyMessage(messages[0].id) + f(.dismissWithoutContent) }))) } if data.canEdit { - actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_Edit, action: { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Edit, icon: { _ in nil }, action: { _, f in interfaceInteraction.setupEditMessage(messages[0].id) + f(.dismissWithoutContent) }))) } @@ -378,31 +386,31 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if !messages[0].text.isEmpty || resourceAvailable { let message = messages[0] - actions.append(.context(ContextMenuAction(content: .text(title: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, accessibilityLabel: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy), action: { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { _ in nil }, action: { _, f in if resourceAvailable { for media in message.media { if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { let _ = (context.account.postbox.mediaBox.resourceData(largest.resource, option: .incremental(waitUntilFetchStatus: false)) - |> take(1) - |> deliverOnMainQueue).start(next: { data in - if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { - if let image = UIImage(data: imageData) { - if !message.text.isEmpty { - UIPasteboard.general.string = message.text - /*UIPasteboard.general.items = [ - [kUTTypeUTF8PlainText as String: message.text], - [kUTTypePNG as String: image] - ]*/ + |> take(1) + |> deliverOnMainQueue).start(next: { data in + if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + if let image = UIImage(data: imageData) { + if !message.text.isEmpty { + UIPasteboard.general.string = message.text + /*UIPasteboard.general.items = [ + [kUTTypeUTF8PlainText as String: message.text], + [kUTTypePNG as String: image] + ]*/ + } else { + UIPasteboard.general.image = image + } } else { - UIPasteboard.general.image = image + UIPasteboard.general.string = message.text } } else { UIPasteboard.general.string = message.text } - } else { - UIPasteboard.general.string = message.text - } - }) + }) } } } else { @@ -415,6 +423,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } storeMessageTextInPasteboard(message.text, entities: messageEntities) } + f(.default) }))) } @@ -433,20 +442,23 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } } if hasSelected { - actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_UnvotePoll, action: { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_UnvotePoll, icon: { _ in nil }, action: { _, f in interfaceInteraction.requestUnvoteInMessage(messages[0].id) + f(.dismissWithoutContent) }))) } } if data.canPin { if chatPresentationInterfaceState.pinnedMessage?.id != messages[0].id { - actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_Pin, action: { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Pin, icon: { _ in nil }, action: { _, f in interfaceInteraction.pinMessage(messages[0].id) + f(.dismissWithoutContent) }))) } else { - actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_Unpin, action: { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Unpin, icon: { _ in nil }, action: { _, f in interfaceInteraction.unpinMessage() + f(.default) }))) } } @@ -477,14 +489,15 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } if canStopPoll { - actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_StopPoll, action: { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_StopPoll, icon: { _ in nil }, action: { _, f in interfaceInteraction.requestStopPollInMessage(messages[0].id) + f(.dismissWithoutContent) }))) } } if let message = messages.first, message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, !(message.media.first is TelegramMediaAction) { - actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopyLink, action: { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopyLink, icon: { _ in nil }, action: { _, f in let _ = (exportMessageLink(account: context.account, peerId: message.id.peerId, messageId: message.id) |> map { result -> String? in return result @@ -501,6 +514,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } } }) + f(.dismissWithoutContent) }))) } @@ -520,8 +534,9 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if let file = media as? TelegramMediaFile { if file.isVideo { if file.isAnimated { - actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_LinkDialogSave, action: { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_LinkDialogSave, icon: { _ in nil }, action: { _, f in let _ = addSavedGif(postbox: context.account.postbox, fileReference: .message(message: MessageReference(message), media: file)).start() + f(.default) }))) } break @@ -530,20 +545,16 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } } } - if data.canSelect { - actions.append(.context(ContextMenuAction(content: .text(title: chatPresentationInterfaceState.strings.Conversation_ContextMenuMore, accessibilityLabel: chatPresentationInterfaceState.strings.Conversation_ContextMenuMore.replacingOccurrences(of: "...", with: "")), action: { - interfaceInteraction.beginMessageSelection(selectAll ? messages.map { $0.id } : [message.id]) - }))) - } if !data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty && isAction { - actions.append(.context(ContextMenuAction(content: .text(title: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, accessibilityLabel: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete), action: { - interfaceInteraction.deleteMessages(messages) + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { _ in nil }, action: { controller, f in + interfaceInteraction.deleteMessages(messages, controller, f) }))) } if data.messageActions.options.contains(.viewStickerPack) { - actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.StickerPack_ViewPack, action: { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.StickerPack_ViewPack, icon: { _ in nil }, action: { _, f in let _ = controllerInteraction.openMessage(message, .default) + f(.dismissWithoutContent) }))) } @@ -565,28 +576,41 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } } if let callId = callId { - actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Call_RateCall, action: { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Call_RateCall, icon: { _ in nil }, action: { _, f in let _ = controllerInteraction.rateCall(message, callId) + f(.dismissWithoutContent) }))) } } if data.messageActions.options.contains(.forward) { - actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_ContextMenuForward, action: { - interfaceInteraction.forwardMessages(selectAll ? messages : [message]) + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuForward, icon: { _ in nil }, action: { _, f in + interfaceInteraction.forwardMessages(selectAll ? messages : [message]) + f(.dismissWithoutContent) }))) } if data.messageActions.options.contains(.report) { - actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_ContextMenuReport, action: { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuReport, icon: { _ in nil }, action: { _, f in interfaceInteraction.reportMessages(selectAll ? messages : [message]) + f(.dismissWithoutContent) }))) } if !data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty && !isAction { let title = message.flags.isSending ? chatPresentationInterfaceState.strings.Conversation_ContextMenuCancelSending : chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete - actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .destructive, title: title, action: { - interfaceInteraction.deleteMessages(selectAll ? messages : [message]) + actions.append(.action(ContextMenuActionItem(text: title, textColor: .destructive, icon: { _ in nil }, action: { controller, f in + interfaceInteraction.deleteMessages(selectAll ? messages : [message], controller, f) + }))) + } + + if data.canSelect { + if !actions.isEmpty { + actions.append(.separator) + } + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuMore, icon: { _ in nil }, action: { _, f in + interfaceInteraction.beginMessageSelection(selectAll ? messages.map { $0.id } : [message.id]) + f(.default) }))) } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift index 2fa6428596..ca331616de 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift @@ -11,6 +11,7 @@ import Compression import TextFormat import AccountContext import StickerResources +import ContextUI private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) @@ -103,6 +104,7 @@ private class ChatMessageHeartbeatHaptic { } class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { + private let contextSourceNode: ContextContentContainingNode let imageNode: TransformImageNode private let animationNode: AnimatedStickerNode private var didSetUpAnimationNode = false @@ -133,6 +135,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var currentSwipeToReplyTranslation: CGFloat = 0.0 required init() { + self.contextSourceNode = ContextContentContainingNode() self.imageNode = TransformImageNode() self.animationNode = AnimatedStickerNode() self.dateAndStatusNode = ChatMessageDateAndStatusNode() @@ -152,9 +155,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } self.imageNode.displaysAsynchronously = false - self.addSubnode(self.imageNode) - self.addSubnode(self.animationNode) - self.addSubnode(self.dateAndStatusNode) + self.addSubnode(self.contextSourceNode) + self.contextSourceNode.contentNode.addSubnode(self.imageNode) + self.contextSourceNode.contentNode.addSubnode(self.animationNode) + self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode) } deinit { @@ -557,6 +561,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _ in if let strongSelf = self { + strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + var transition: ContainedViewLayoutTransition = .immediate if case let .System(duration) = animation { transition = .animated(duration: duration, curve: .spring) @@ -573,6 +580,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size) imageApply() + strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame + if let updatedShareButtonNode = updatedShareButtonNode { if updatedShareButtonNode !== strongSelf.shareButtonNode { if let shareButtonNode = strongSelf.shareButtonNode { @@ -1008,4 +1017,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } + + override func getMessageContextSourceNode() -> ContextContentContainingNode? { + return self.contextSourceNode + } + + override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { + self.contextSourceNode.contentNode.addSubnode(accessoryItemNode) + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBackground.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBackground.swift index c7060d854f..1694deb2bb 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBackground.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBackground.swift @@ -59,8 +59,9 @@ enum ChatMessageBackgroundType: Equatable { class ChatMessageBackground: ASImageNode { private(set) var type: ChatMessageBackgroundType? - private var currentHighlighted = false + private var currentHighlighted: Bool? private var graphics: PrincipalThemeEssentialGraphics? + private var maskMode: Bool? override init() { super.init() @@ -70,9 +71,15 @@ class ChatMessageBackground: ASImageNode { self.displayWithoutProcessing = true } - func setType(type: ChatMessageBackgroundType, highlighted: Bool, graphics: PrincipalThemeEssentialGraphics, transition: ContainedViewLayoutTransition) { + func setMaskMode(_ maskMode: Bool) { + if let type = self.type, let highlighted = self.currentHighlighted, let graphics = self.graphics { + self.setType(type: type, highlighted: highlighted, graphics: graphics, maskMode: maskMode, transition: .immediate) + } + } + + func setType(type: ChatMessageBackgroundType, highlighted: Bool, graphics: PrincipalThemeEssentialGraphics, maskMode: Bool, transition: ContainedViewLayoutTransition) { let previousType = self.type - if let currentType = previousType, currentType == type, self.currentHighlighted == highlighted, self.graphics === graphics { + if let currentType = previousType, currentType == type, self.currentHighlighted == highlighted, self.graphics === graphics, self.maskMode == maskMode { return } self.type = type @@ -81,42 +88,50 @@ class ChatMessageBackground: ASImageNode { let image: UIImage? switch type { - case .none: + case .none: + image = nil + case let .incoming(mergeType): + if maskMode && graphics.incomingBubbleGradientImage != nil { image = nil - case let .incoming(mergeType): + } else { switch mergeType { - case .None: - image = highlighted ? graphics.chatMessageBackgroundIncomingHighlightedImage : graphics.chatMessageBackgroundIncomingImage - case let .Top(side): - if side { - image = highlighted ? graphics.chatMessageBackgroundIncomingMergedTopSideHighlightedImage : graphics.chatMessageBackgroundIncomingMergedTopSideImage - } else { - image = highlighted ? graphics.chatMessageBackgroundIncomingMergedTopHighlightedImage : graphics.chatMessageBackgroundIncomingMergedTopImage - } - case .Bottom: - image = highlighted ? graphics.chatMessageBackgroundIncomingMergedBottomHighlightedImage : graphics.chatMessageBackgroundIncomingMergedBottomImage - case .Both: - image = highlighted ? graphics.chatMessageBackgroundIncomingMergedBothHighlightedImage : graphics.chatMessageBackgroundIncomingMergedBothImage - case .Side: - image = highlighted ? graphics.chatMessageBackgroundIncomingMergedSideHighlightedImage : graphics.chatMessageBackgroundIncomingMergedSideImage + case .None: + image = highlighted ? graphics.chatMessageBackgroundIncomingHighlightedImage : graphics.chatMessageBackgroundIncomingImage + case let .Top(side): + if side { + image = highlighted ? graphics.chatMessageBackgroundIncomingMergedTopSideHighlightedImage : graphics.chatMessageBackgroundIncomingMergedTopSideImage + } else { + image = highlighted ? graphics.chatMessageBackgroundIncomingMergedTopHighlightedImage : graphics.chatMessageBackgroundIncomingMergedTopImage + } + case .Bottom: + image = highlighted ? graphics.chatMessageBackgroundIncomingMergedBottomHighlightedImage : graphics.chatMessageBackgroundIncomingMergedBottomImage + case .Both: + image = highlighted ? graphics.chatMessageBackgroundIncomingMergedBothHighlightedImage : graphics.chatMessageBackgroundIncomingMergedBothImage + case .Side: + image = highlighted ? graphics.chatMessageBackgroundIncomingMergedSideHighlightedImage : graphics.chatMessageBackgroundIncomingMergedSideImage } - case let .outgoing(mergeType): + } + case let .outgoing(mergeType): + if maskMode && graphics.outgoingBubbleGradientImage != nil { + image = nil + } else { switch mergeType { - case .None: - image = highlighted ? graphics.chatMessageBackgroundOutgoingHighlightedImage : graphics.chatMessageBackgroundOutgoingImage - case let .Top(side): - if side { - image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedTopSideHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedTopSideImage - } else { - image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedTopHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedTopImage - } - case .Bottom: - image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedBottomHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedBottomImage - case .Both: - image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedBothHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedBothImage - case .Side: - image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedSideHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedSideImage + case .None: + image = highlighted ? graphics.chatMessageBackgroundOutgoingHighlightedImage : graphics.chatMessageBackgroundOutgoingImage + case let .Top(side): + if side { + image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedTopSideHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedTopSideImage + } else { + image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedTopHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedTopImage + } + case .Bottom: + image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedBottomHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedBottomImage + case .Both: + image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedBothHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedBothImage + case .Side: + image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedSideHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedSideImage } + } } if let previousType = previousType, previousType != .none, type == .none { diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleBackdrop.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleBackdrop.swift index 204c9fe4c5..4cd5130ea7 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleBackdrop.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleBackdrop.swift @@ -8,7 +8,17 @@ final class ChatMessageBubbleBackdrop: ASDisplayNode { private let backgroundContent: ASDisplayNode private var currentType: ChatMessageBackgroundType? + private var currentMaskMode: Bool? private var theme: ChatPresentationThemeData? + private var essentialGraphics: PrincipalThemeEssentialGraphics? + + private var maskView: UIImageView? + + override var frame: CGRect { + didSet { + self.maskView?.frame = self.bounds + } + } override init() { self.backgroundContent = ASDisplayNode() @@ -20,10 +30,81 @@ final class ChatMessageBubbleBackdrop: ASDisplayNode { self.addSubnode(self.backgroundContent) } - func setType(type: ChatMessageBackgroundType, theme: ChatPresentationThemeData, mediaBox: MediaBox, essentialGraphics: PrincipalThemeEssentialGraphics) { - if self.currentType != type || self.theme != theme { + private func maskForType(_ type: ChatMessageBackgroundType, graphics: PrincipalThemeEssentialGraphics) -> UIImage? { + let image: UIImage? + switch type { + case .none: + image = nil + case let .incoming(mergeType): + switch mergeType { + case .None: + image = graphics.chatMessageBackgroundIncomingMaskImage + case let .Top(side): + if side { + image = graphics.chatMessageBackgroundIncomingMergedTopSideMaskImage + } else { + image = graphics.chatMessageBackgroundIncomingMergedTopMaskImage + } + case .Bottom: + image = graphics.chatMessageBackgroundIncomingMergedBottomMaskImage + case .Both: + image = graphics.chatMessageBackgroundIncomingMergedBothMaskImage + case .Side: + image = graphics.chatMessageBackgroundIncomingMergedSideMaskImage + } + case let .outgoing(mergeType): + switch mergeType { + case .None: + image = graphics.chatMessageBackgroundOutgoingMaskImage + case let .Top(side): + if side { + image = graphics.chatMessageBackgroundOutgoingMergedTopSideMaskImage + } else { + image = graphics.chatMessageBackgroundOutgoingMergedTopMaskImage + } + case .Bottom: + image = graphics.chatMessageBackgroundOutgoingMergedBottomMaskImage + case .Both: + image = graphics.chatMessageBackgroundOutgoingMergedBothMaskImage + case .Side: + image = graphics.chatMessageBackgroundOutgoingMergedSideMaskImage + } + } + return image + } + + func setMaskMode(_ maskMode: Bool, mediaBox: MediaBox) { + if let currentType = self.currentType, let theme = self.theme, let essentialGraphics = self.essentialGraphics { + self.setType(type: currentType, theme: theme, mediaBox: mediaBox, essentialGraphics: essentialGraphics, maskMode: maskMode) + } + } + + func setType(type: ChatMessageBackgroundType, theme: ChatPresentationThemeData, mediaBox: MediaBox, essentialGraphics: PrincipalThemeEssentialGraphics, maskMode: Bool) { + if self.currentType != type || self.theme != theme || self.currentMaskMode != maskMode { self.currentType = type self.theme = theme + self.essentialGraphics = essentialGraphics + + if maskMode != self.currentMaskMode { + self.currentMaskMode = maskMode + + if maskMode { + let maskView: UIImageView + if let current = self.maskView { + maskView = current + } else { + maskView = UIImageView() + maskView.frame = self.bounds + self.maskView = maskView + self.view.mask = maskView + } + } else { + if let _ = self.maskView { + self.view.mask = nil + self.maskView = nil + } + } + } switch type { case .none: @@ -33,6 +114,10 @@ final class ChatMessageBubbleBackdrop: ASDisplayNode { case .outgoing: self.backgroundContent.contents = essentialGraphics.outgoingBubbleGradientImage?.cgImage } + + if let maskView = self.maskView { + maskView.image = self.maskForType(type, graphics: essentialGraphics) + } } } @@ -44,4 +129,8 @@ final class ChatMessageBubbleBackdrop: ASDisplayNode { let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: animationCurve) transition.animatePositionAdditive(node: self.backgroundContent, offset: CGPoint(x: 0.0, y: -value)) } + + func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + self.backgroundContent.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: 0.0, y: value)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: duration, initialVelocity: 0.0, damping: damping, additive: true) + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift index aa60aa715b..719dfe3156 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift @@ -10,6 +10,7 @@ import TextFormat import AccountContext import TemporaryCachedPeerDataManager import LocalizedPeerData +import ContextUI private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(Message, AnyClass)] { var result: [(Message, AnyClass)] = [] @@ -132,6 +133,7 @@ private enum ContentNodeOperation { } class ChatMessageBubbleItemNode: ChatMessageItemView { + private let contextSourceNode: ContextContentContainingNode private let backgroundWallpaperNode: ChatMessageBubbleBackdrop private let backgroundNode: ChatMessageBackground private var transitionClippingNode: ASDisplayNode? @@ -176,6 +178,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } required init() { + self.contextSourceNode = ContextContentContainingNode() self.backgroundWallpaperNode = ChatMessageBubbleBackdrop() self.backgroundNode = ChatMessageBackground() @@ -183,8 +186,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { super.init(layerBacked: false) - self.addSubnode(self.backgroundWallpaperNode) - self.addSubnode(self.backgroundNode) + self.addSubnode(self.contextSourceNode) + self.contextSourceNode.contentNode.addSubnode(self.backgroundWallpaperNode) + self.contextSourceNode.contentNode.addSubnode(self.backgroundNode) self.addSubnode(self.messageAccessibilityArea) self.messageAccessibilityArea.activate = { [weak self] in @@ -200,6 +204,38 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { self.messageAccessibilityArea.focused = { [weak self] in self?.accessibilityElementDidBecomeFocused() } + + self.contextSourceNode.isExtractedToContextPreviewUpdated = { [weak self] isExtractedToContextPreview in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + strongSelf.backgroundWallpaperNode.setMaskMode(isExtractedToContextPreview, mediaBox: item.context.account.postbox.mediaBox) + strongSelf.backgroundNode.setMaskMode(isExtractedToContextPreview) + if !isExtractedToContextPreview, let (originalRect, size) = strongSelf.absoluteRect { + var rect = originalRect + rect.origin.y = size.height - rect.maxY - strongSelf.insets.top + strongSelf.updateAbsoluteRectInternal(rect, within: size) + } + } + + self.contextSourceNode.updateAbsoluteRect = { [weak self] rect, size in + guard let strongSelf = self, strongSelf.contextSourceNode.isExtractedToContextPreview else { + return + } + strongSelf.updateAbsoluteRectInternal(rect, within: size) + } + self.contextSourceNode.applyAbsoluteOffset = { [weak self] value, animationCurve, duration in + guard let strongSelf = self, strongSelf.contextSourceNode.isExtractedToContextPreview else { + return + } + strongSelf.applyAbsoluteOffsetInternal(value: value, animationCurve: animationCurve, duration: duration) + } + self.contextSourceNode.applyAbsoluteOffsetSpring = { [weak self] value, duration, damping in + guard let strongSelf = self, strongSelf.contextSourceNode.isExtractedToContextPreview else { + return + } + strongSelf.applyAbsoluteOffsetSpringInternal(value: value, duration: duration, damping: damping) + } } required init?(coder aDecoder: NSCoder) { @@ -1355,6 +1391,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { guard let strongSelf = selfReference.value else { return } + strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.appliedItem = item strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) strongSelf.updateAccessibilityData(accessibilityData) @@ -1379,8 +1418,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } else { backgroundType = .incoming(mergeType) } - strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics, transition: transition) - strongSelf.backgroundWallpaperNode.setType(type: backgroundType, theme: item.presentationData.theme, mediaBox: item.context.account.postbox.mediaBox, essentialGraphics: graphics) + strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics, maskMode: strongSelf.contextSourceNode.isExtractedToContextPreview, transition: transition) + strongSelf.backgroundWallpaperNode.setType(type: backgroundType, theme: item.presentationData.theme, mediaBox: item.context.account.postbox.mediaBox, essentialGraphics: graphics, maskMode: strongSelf.contextSourceNode.isExtractedToContextPreview) strongSelf.backgroundType = backgroundType @@ -1421,7 +1460,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if !nameNode.isNodeLoaded { nameNode.isUserInteractionEnabled = false } - strongSelf.insertSubnode(nameNode, belowSubnode: strongSelf.messageAccessibilityArea) + strongSelf.contextSourceNode.contentNode.addSubnode(nameNode) } nameNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + nameNodeOriginY), size: nameNodeSizeApply.0) @@ -1432,7 +1471,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } else { credibilityIconNode = ASImageNode() strongSelf.credibilityIconNode = credibilityIconNode - strongSelf.insertSubnode(credibilityIconNode, belowSubnode: strongSelf.messageAccessibilityArea) + strongSelf.contextSourceNode.contentNode.addSubnode(credibilityIconNode) } credibilityIconNode.frame = CGRect(origin: CGPoint(x: nameNode.frame.maxX + 4.0, y: nameNode.frame.minY), size: credibilityIconImage.size) credibilityIconNode.image = credibilityIconImage @@ -1448,7 +1487,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if !adminBadgeNode.isNodeLoaded { adminBadgeNode.isUserInteractionEnabled = false } - strongSelf.insertSubnode(adminBadgeNode, belowSubnode: strongSelf.messageAccessibilityArea) + strongSelf.contextSourceNode.contentNode.addSubnode(adminBadgeNode) adminBadgeNode.frame = adminBadgeFrame } else { let previousAdminBadgeFrame = adminBadgeNode.frame @@ -1470,7 +1509,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { strongSelf.forwardInfoNode = forwardInfoNode var animateFrame = true if forwardInfoNode.supernode == nil { - strongSelf.insertSubnode(forwardInfoNode, belowSubnode: strongSelf.messageAccessibilityArea) + strongSelf.contextSourceNode.contentNode.addSubnode(forwardInfoNode) animateFrame = false } let previousForwardInfoNodeFrame = forwardInfoNode.frame @@ -1489,7 +1528,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { strongSelf.replyInfoNode = replyInfoNode var animateFrame = true if replyInfoNode.supernode == nil { - strongSelf.insertSubnode(replyInfoNode, belowSubnode: strongSelf.messageAccessibilityArea) + strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) animateFrame = false } let previousReplyInfoNodeFrame = replyInfoNode.frame @@ -1526,7 +1565,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let addedContentNodes = addedContentNodes { for (_, contentNode) in addedContentNodes { updatedContentNodes.append(contentNode) - strongSelf.insertSubnode(contentNode, belowSubnode: strongSelf.messageAccessibilityArea) + strongSelf.contextSourceNode.contentNode.addSubnode(contentNode) contentNode.visibility = strongSelf.visibility } @@ -1598,7 +1637,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if mosaicStatusNode !== strongSelf.mosaicStatusNode { strongSelf.mosaicStatusNode?.removeFromSupernode() strongSelf.mosaicStatusNode = mosaicStatusNode - strongSelf.insertSubnode(mosaicStatusNode, aboveSubnode: strongSelf.messageAccessibilityArea) + strongSelf.contextSourceNode.contentNode.addSubnode(mosaicStatusNode) } let absoluteOrigin = mosaicStatusOrigin.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y) mosaicStatusNode.frame = CGRect(origin: CGPoint(x: absoluteOrigin.x - layoutConstants.image.statusInsets.right - size.width, y: absoluteOrigin.y - layoutConstants.image.statusInsets.bottom - size.height), size: size) @@ -1639,6 +1678,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { strongSelf.backgroundFrameTransition = nil } strongSelf.backgroundNode.frame = backgroundFrame + var incomingOffset: CGFloat = 0.0 + switch backgroundType { + case .incoming: + incomingOffset = 5.0 + default: + break + } + strongSelf.contextSourceNode.contentRect = backgroundFrame.offsetBy(dx: incomingOffset, dy: 0.0) strongSelf.backgroundWallpaperNode.frame = backgroundFrame if let (rect, size) = strongSelf.absoluteRect { strongSelf.updateAbsoluteRect(rect, within: size) @@ -1733,7 +1780,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let transitionClippingNode = self.transitionClippingNode { transitionClippingNode.addSubnode(node) } else { - self.insertSubnode(node, belowSubnode: self.messageAccessibilityArea) + self.contextSourceNode.contentNode.addSubnode(node) } } @@ -1754,7 +1801,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { for contentNode in self.contentNodes { node.addSubnode(contentNode) } - self.insertSubnode(node, belowSubnode: self.messageAccessibilityArea) + self.contextSourceNode.contentNode.addSubnode(node) self.transitionClippingNode = node } } @@ -1762,13 +1809,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { private func disableTransitionClippingNode() { if let transitionClippingNode = self.transitionClippingNode { if let forwardInfoNode = self.forwardInfoNode { - self.insertSubnode(forwardInfoNode, belowSubnode: self.messageAccessibilityArea) + self.contextSourceNode.contentNode.addSubnode(forwardInfoNode) } if let replyInfoNode = self.replyInfoNode { - self.insertSubnode(replyInfoNode, belowSubnode: self.messageAccessibilityArea) + self.contextSourceNode.contentNode.addSubnode(replyInfoNode) } for contentNode in self.contentNodes { - self.insertSubnode(contentNode, belowSubnode: self.messageAccessibilityArea) + self.contextSourceNode.contentNode.addSubnode(contentNode) } transitionClippingNode.removeFromSupernode() self.transitionClippingNode = nil @@ -1790,6 +1837,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let backgroundFrame = CGRect.interpolator()(backgroundFrameTransition.0, backgroundFrameTransition.1, progress) as! CGRect self.backgroundNode.frame = backgroundFrame self.backgroundWallpaperNode.frame = backgroundFrame + if let type = self.backgroundNode.type { + var incomingOffset: CGFloat = 0.0 + switch type { + case .incoming: + incomingOffset = 5.0 + default: + break + } + self.contextSourceNode.contentRect = backgroundFrame.offsetBy(dx: incomingOffset, dy: 0.0) + } if let (rect, size) = self.absoluteRect { self.updateAbsoluteRect(rect, within: size) } @@ -2363,16 +2420,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let graphics = PresentationResourcesChat.principalGraphics(mediaBox: item.context.account.postbox.mediaBox, knockoutWallpaper: item.context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) if highlighted { - self.backgroundNode.setType(type: backgroundType, highlighted: true, graphics: graphics, transition: .immediate) + self.backgroundNode.setType(type: backgroundType, highlighted: true, graphics: graphics, maskMode: self.contextSourceNode.isExtractedToContextPreview, transition: .immediate) } else { if let previousContents = self.backgroundNode.layer.contents, animated { - self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, transition: .immediate) + self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, maskMode: self.contextSourceNode.isExtractedToContextPreview, transition: .immediate) if let updatedContents = self.backgroundNode.layer.contents { self.backgroundNode.layer.animate(from: previousContents as AnyObject, to: updatedContents as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.42) } } else { - self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, transition: .immediate) + self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, maskMode: self.contextSourceNode.isExtractedToContextPreview, transition: .immediate) } } } @@ -2461,11 +2518,37 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteRect = (rect, containerSize) - let mappedRect = CGRect(origin: CGPoint(x: rect.minX + self.backgroundWallpaperNode.frame.minX, y: containerSize.height - rect.maxY + self.backgroundWallpaperNode.frame.minY), size: rect.size) + if !self.contextSourceNode.isExtractedToContextPreview { + var rect = rect + rect.origin.y = containerSize.height - rect.maxY + self.insets.top + self.updateAbsoluteRectInternal(rect, within: containerSize) + } + } + + private func updateAbsoluteRectInternal(_ rect: CGRect, within containerSize: CGSize) { + let mappedRect = CGRect(origin: CGPoint(x: rect.minX + self.backgroundWallpaperNode.frame.minX, y: rect.minY + self.backgroundWallpaperNode.frame.minY), size: rect.size) self.backgroundWallpaperNode.update(rect: mappedRect, within: containerSize) } override func applyAbsoluteOffset(value: CGFloat, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { - self.backgroundWallpaperNode.offset(value: -value, animationCurve: animationCurve, duration: duration) + if !self.contextSourceNode.isExtractedToContextPreview { + self.applyAbsoluteOffsetInternal(value: -value, animationCurve: animationCurve, duration: duration) + } + } + + private func applyAbsoluteOffsetInternal(value: CGFloat, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + self.backgroundWallpaperNode.offset(value: value, animationCurve: animationCurve, duration: duration) + } + + private func applyAbsoluteOffsetSpringInternal(value: CGFloat, duration: Double, damping: CGFloat) { + self.backgroundWallpaperNode.offsetSpring(value: value, duration: duration, damping: damping) + } + + override func getMessageContextSourceNode() -> ContextContentContainingNode? { + return self.contextSourceNode + } + + override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { + self.contextSourceNode.contentNode.addSubnode(accessoryItemNode) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageContextControllerContentSource.swift b/submodules/TelegramUI/TelegramUI/ChatMessageContextControllerContentSource.swift new file mode 100644 index 0000000000..d59840356c --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/ChatMessageContextControllerContentSource.swift @@ -0,0 +1,55 @@ +import Foundation +import UIKit +import Display +import ContextUI +import Postbox + +final class ChatMessageContextControllerContentSource: ContextControllerContentSource { + private weak var chatNode: ChatControllerNode? + private let message: Message + + init(chatNode: ChatControllerNode, message: Message) { + self.chatNode = chatNode + self.message = message + } + + func takeView() -> ContextControllerTakeViewInfo? { + guard let chatNode = self.chatNode else { + return nil + } + + var result: ContextControllerTakeViewInfo? + chatNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.message.stableId == self.message.stableId, let contentNode = itemNode.getMessageContextSourceNode() { + result = ContextControllerTakeViewInfo(contentContainingNode: contentNode, contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + } + } + return result + } + + func putBack() -> ContextControllerPutBackViewInfo? { + guard let chatNode = self.chatNode else { + return nil + } + + var result: ContextControllerPutBackViewInfo? + chatNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.message.stableId == self.message.stableId { + result = ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + } + } + return result + } +} diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageInstantVideoItemNode.swift index 2736a1d81d..3efcc94dc1 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -10,6 +10,7 @@ import TelegramUIPreferences import TextFormat import AccountContext import LocalizedPeerData +import ContextUI private let nameFont = Font.medium(14.0) @@ -17,6 +18,7 @@ private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotNameFont = nameFont class ChatMessageInstantVideoItemNode: ChatMessageItemView { + private let contextSourceNode: ContextContentContainingNode private let interactiveVideoNode: ChatMessageInteractiveInstantVideoNode private var selectionNode: ChatMessageSelectionNode? @@ -52,11 +54,13 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } required init() { + self.contextSourceNode = ContextContentContainingNode() self.interactiveVideoNode = ChatMessageInteractiveInstantVideoNode() super.init(layerBacked: false) - self.addSubnode(self.interactiveVideoNode) + self.addSubnode(self.contextSourceNode) + self.contextSourceNode.contentNode.addSubnode(self.interactiveVideoNode) } required init?(coder aDecoder: NSCoder) { @@ -376,6 +380,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _ in if let strongSelf = self { + strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + strongSelf.appliedItem = item strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) @@ -394,6 +401,8 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } videoApply(videoLayoutData, transition) + strongSelf.contextSourceNode.contentRect = videoFrame + if let updatedShareButtonNode = updatedShareButtonNode { if updatedShareButtonNode !== strongSelf.shareButtonNode { if let shareButtonNode = strongSelf.shareButtonNode { @@ -815,4 +824,12 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { return self.interactiveVideoNode.playMediaWithSound() } + + override func getMessageContextSourceNode() -> ContextContentContainingNode? { + return self.contextSourceNode + } + + override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { + self.contextSourceNode.contentNode.addSubnode(accessoryItemNode) + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageItemView.swift b/submodules/TelegramUI/TelegramUI/ChatMessageItemView.swift index 35f5ce0414..b4643abeec 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageItemView.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageItemView.swift @@ -6,6 +6,7 @@ import Postbox import TelegramCore import AccountContext import LocalizedPeerData +import ContextUI struct ChatMessageItemWidthFill { let compactInset: CGFloat @@ -675,6 +676,10 @@ public class ChatMessageItemView: ListViewItemNode { return nil } + func getMessageContextSourceNode() -> ContextContentContainingNode? { + return nil + } + func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { return nil } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageStickerItemNode.swift index 4ceb94f8d9..c62895063c 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageStickerItemNode.swift @@ -9,12 +9,14 @@ import TelegramPresentationData import TextFormat import AccountContext import StickerResources +import ContextUI private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotNameFont = nameFont class ChatMessageStickerItemNode: ChatMessageItemView { + private let contextSourceNode: ContextContentContainingNode let imageNode: TransformImageNode var textNode: TextNode? @@ -40,14 +42,16 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private var currentSwipeToReplyTranslation: CGFloat = 0.0 required init() { + self.contextSourceNode = ContextContentContainingNode() self.imageNode = TransformImageNode() self.dateAndStatusNode = ChatMessageDateAndStatusNode() super.init(layerBacked: false) self.imageNode.displaysAsynchronously = false - self.addSubnode(self.imageNode) - self.addSubnode(self.dateAndStatusNode) + self.addSubnode(self.contextSourceNode) + self.contextSourceNode.contentNode.addSubnode(self.imageNode) + self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode) } required init?(coder aDecoder: NSCoder) { @@ -379,6 +383,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView { return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _ in if let strongSelf = self { + strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + var transition: ContainedViewLayoutTransition = .immediate if case let .System(duration) = animation { transition = .animated(duration: duration, curve: .spring) @@ -388,6 +395,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { transition.updateFrame(node: strongSelf.imageNode, frame: updatedImageFrame) imageApply() + strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame + dateAndStatusApply(false) var dateOffset = CGPoint(x: dateAndStatusSize.width + 4.0, y: dateAndStatusSize.height + 16.0) @@ -857,4 +866,12 @@ class ChatMessageStickerItemNode: ChatMessageItemView { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } + + override func getMessageContextSourceNode() -> ContextContentContainingNode? { + return self.contextSourceNode + } + + override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { + self.contextSourceNode.contentNode.addSubnode(accessoryItemNode) + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatPanelInterfaceInteraction.swift b/submodules/TelegramUI/TelegramUI/ChatPanelInterfaceInteraction.swift index babac77bb7..86f6796f03 100644 --- a/submodules/TelegramUI/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/submodules/TelegramUI/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -5,6 +5,7 @@ import SwiftSignalKit import TelegramCore import Display import AccountContext +import ContextUI public enum ChatFinishMediaRecordingAction { case dismiss @@ -50,7 +51,7 @@ final class ChatPanelInterfaceInteraction { let deleteSelectedMessages: () -> Void let reportSelectedMessages: () -> Void let reportMessages: ([Message]) -> Void - let deleteMessages: ([Message]) -> Void + let deleteMessages: ([Message], ContextController?, @escaping (ContextMenuActionResult) -> Void) -> Void let forwardSelectedMessages: () -> Void let forwardCurrentForwardMessages: () -> Void let forwardMessages: ([Message]) -> Void @@ -110,7 +111,7 @@ final class ChatPanelInterfaceInteraction { let displaySendMessageOptions: () -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId?) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, reportMessages: @escaping ([Message]) -> Void, deleteMessages: @escaping ([Message]) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardCurrentForwardMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, openStickers: @escaping () -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, navigateToChat: @escaping (PeerId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void, displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, shareAccountContact: @escaping () -> Void, reportPeer: @escaping () -> Void, presentPeerContact: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, requestUnvoteInMessage: @escaping (MessageId) -> Void, requestStopPollInMessage: @escaping (MessageId) -> Void, updateInputLanguage: @escaping ((String?) -> String?) -> Void, unarchiveChat: @escaping () -> Void, openLinkEditing: @escaping () -> Void, reportPeerIrrelevantGeoLocation: @escaping () -> Void, displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, displaySendMessageOptions: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId?) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, reportMessages: @escaping ([Message]) -> Void, deleteMessages: @escaping ([Message], ContextController?, @escaping (ContextMenuActionResult) -> Void) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardCurrentForwardMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, openStickers: @escaping () -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, navigateToChat: @escaping (PeerId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void, displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, shareAccountContact: @escaping () -> Void, reportPeer: @escaping () -> Void, presentPeerContact: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, requestUnvoteInMessage: @escaping (MessageId) -> Void, requestStopPollInMessage: @escaping (MessageId) -> Void, updateInputLanguage: @escaping ((String?) -> String?) -> Void, unarchiveChat: @escaping () -> Void, openLinkEditing: @escaping () -> Void, reportPeerIrrelevantGeoLocation: @escaping () -> Void, displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, displaySendMessageOptions: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection diff --git a/submodules/TelegramUI/TelegramUI/ChatRecentActionsController.swift b/submodules/TelegramUI/TelegramUI/ChatRecentActionsController.swift index 005efebfd9..7307a0d168 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecentActionsController.swift @@ -47,7 +47,8 @@ final class ChatRecentActionsController: TelegramBaseController { }, deleteSelectedMessages: { }, reportSelectedMessages: { }, reportMessages: { _ in - }, deleteMessages: { _ in + }, deleteMessages: { _, _, f in + f(.default) }, forwardSelectedMessages: { }, forwardCurrentForwardMessages: { }, forwardMessages: { _ in diff --git a/submodules/TelegramUI/TelegramUI/FetchCachedRepresentations.swift b/submodules/TelegramUI/TelegramUI/FetchCachedRepresentations.swift index 84ec4e6c29..8acfe5e9db 100644 --- a/submodules/TelegramUI/TelegramUI/FetchCachedRepresentations.swift +++ b/submodules/TelegramUI/TelegramUI/FetchCachedRepresentations.swift @@ -78,7 +78,7 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR return .complete() } return fetchCachedPatternWallpaperRepresentation(account: account, resource: resource, resourceData: data, representation: representation) - } + } } else if let representation = representation as? CachedAlbumArtworkRepresentation { return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) |> mapToSignal { data -> Signal in diff --git a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift index 3a8078a77f..1c60f92110 100644 --- a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift +++ b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift @@ -297,7 +297,8 @@ public class PeerMediaCollectionController: TelegramBaseController { }), in: .window(.root)) } }, reportMessages: { _ in - }, deleteMessages: { _ in + }, deleteMessages: { _, _, f in + f(.default) }, forwardSelectedMessages: { [weak self] in if let strongSelf = self { if let forwardMessageIdsSet = strongSelf.interfaceState.selectionState?.selectedIds { diff --git a/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj b/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj index fc15625414..c45fd565f0 100644 --- a/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj +++ b/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj @@ -506,6 +506,9 @@ D09E63AA1F0FC681003444CD /* PictureInPictureVideoControlsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E63A91F0FC681003444CD /* PictureInPictureVideoControlsNode.swift */; }; D09E63B01F1010FE003444CD /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E63AF1F1010FE003444CD /* Contacts.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; D09E63B21F11289A003444CD /* PassKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E63B11F11289A003444CD /* PassKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + D09E778722F8E9ED00B9CCA7 /* ContextUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E778622F8E9ED00B9CCA7 /* ContextUI.framework */; }; + D09E778B22F988C000B9CCA7 /* MediaResources.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E778A22F988C000B9CCA7 /* MediaResources.framework */; }; + D09E778F22FA239B00B9CCA7 /* ChatMessageContextControllerContentSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E778E22FA239A00B9CCA7 /* ChatMessageContextControllerContentSource.swift */; }; D09F9DCF20768DAF00DB4DE1 /* SecureIdLocalResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09F9DCE20768DAF00DB4DE1 /* SecureIdLocalResource.swift */; }; D0A8998D217A294100759EE6 /* SaveIncomingMediaController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A8998C217A294100759EE6 /* SaveIncomingMediaController.swift */; }; D0A8BBA11F61EE83000F03FD /* UniversalVideoGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A8BBA01F61EE83000F03FD /* UniversalVideoGalleryItem.swift */; }; @@ -1832,6 +1835,9 @@ D09E63A91F0FC681003444CD /* PictureInPictureVideoControlsNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PictureInPictureVideoControlsNode.swift; sourceTree = ""; }; D09E63AF1F1010FE003444CD /* Contacts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Contacts.framework; path = System/Library/Frameworks/Contacts.framework; sourceTree = SDKROOT; }; D09E63B11F11289A003444CD /* PassKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PassKit.framework; path = System/Library/Frameworks/PassKit.framework; sourceTree = SDKROOT; }; + D09E778622F8E9ED00B9CCA7 /* ContextUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ContextUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D09E778A22F988C000B9CCA7 /* MediaResources.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MediaResources.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D09E778E22FA239A00B9CCA7 /* ChatMessageContextControllerContentSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageContextControllerContentSource.swift; sourceTree = ""; }; D09F9DCE20768DAF00DB4DE1 /* SecureIdLocalResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdLocalResource.swift; sourceTree = ""; }; D0A11BF91E7836C20081CE03 /* ChangePhoneNumberIntroController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberIntroController.swift; sourceTree = ""; }; D0A11BFB1E7840750081CE03 /* ChangePhoneNumberController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberController.swift; sourceTree = ""; }; @@ -2346,6 +2352,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D09E778B22F988C000B9CCA7 /* MediaResources.framework in Frameworks */, + D09E778722F8E9ED00B9CCA7 /* ContextUI.framework in Frameworks */, D038ACD722F8BE9700320981 /* UnreadSearchBadge.framework in Frameworks */, D038AC5322F88A3600320981 /* ImageBlur.framework in Frameworks */, D0879CCC22F876DD00C4D6B3 /* ChatListSearchRecentPeersNode.framework in Frameworks */, @@ -3430,6 +3438,8 @@ D08D45281D5E340200A7428A /* Frameworks */ = { isa = PBXGroup; children = ( + D09E778A22F988C000B9CCA7 /* MediaResources.framework */, + D09E778622F8E9ED00B9CCA7 /* ContextUI.framework */, D038ACD622F8BE9700320981 /* UnreadSearchBadge.framework */, D038AC5222F88A3600320981 /* ImageBlur.framework */, D0879CCB22F876DD00C4D6B3 /* ChatListSearchRecentPeersNode.framework */, @@ -4488,6 +4498,7 @@ D025402622E1F23000AC0195 /* ChatSendButtonRadialStatusNode.swift */, D025402822E1F7F500AC0195 /* ChatTextInputSlowmodePlaceholderNode.swift */, D06018B422F3659900796784 /* ChatTextFormat.swift */, + D09E778E22FA239A00B9CCA7 /* ChatMessageContextControllerContentSource.swift */, ); name = Chat; sourceTree = ""; @@ -5928,6 +5939,7 @@ 09F79A0121C8116C00820234 /* WebSearchBadgeNode.swift in Sources */, D0CB27CF20C17A4A001ACF93 /* TermsOfServiceController.swift in Sources */, D00BDA1F1EE5B69200C64C5E /* ChannelAdminController.swift in Sources */, + D09E778F22FA239B00B9CCA7 /* ChatMessageContextControllerContentSource.swift in Sources */, D0EC6E501EB9F58900EBF1C3 /* ChannelAdminsController.swift in Sources */, D0EC6E511EB9F58900EBF1C3 /* ChannelBlacklistController.swift in Sources */, D0EC6E521EB9F58900EBF1C3 /* ChannelInfoController.swift in Sources */,