First context menu implementation

This commit is contained in:
Peter 2019-08-07 04:02:38 +03:00
parent 287dfb86ae
commit 3926531c39
31 changed files with 1414 additions and 191 deletions

View File

@ -76,6 +76,9 @@
<Group
location = "container:"
name = "Resources">
<FileRef
location = "group:submodules/MediaResources/MediaResources_Xcode.xcodeproj">
</FileRef>
<FileRef
location = "group:submodules/StickerResources/StickerResources_Xcode.xcodeproj">
</FileRef>
@ -170,9 +173,6 @@
<FileRef
location = "group:submodules/RMIntro/RMIntro_Xcode.xcodeproj">
</FileRef>
<FileRef
location = "group:submodules/MediaResources/MediaResources_Xcode.xcodeproj">
</FileRef>
<FileRef
location = "group:submodules/CheckNode/CheckNode_Xcode.xcodeproj">
</FileRef>

View File

@ -79,7 +79,6 @@
buildConfiguration = "DebugHockeyapp"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableAddressSanitizer = "YES"
enableASanStackUseAfterReturn = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"

View File

@ -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 = "<group>"; };
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 = "<group>"; };
D09E778222F8E67300B9CCA7 /* ContextActionNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextActionNode.swift; sourceTree = "<group>"; };
D09E778422F8E83600B9CCA7 /* ContextContentContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextContentContainerNode.swift; sourceTree = "<group>"; };
D09E778C22FA055100B9CCA7 /* ContextContentSourceNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextContentSourceNode.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
@ -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;
};

View File

@ -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)
})
}
}

View File

@ -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)
}
}

View File

@ -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) {
}
}

View File

@ -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 {
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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<MessageId>, options: ChatAvailableMessageActionOptions) {
private func presentDeleteMessageOptions(messageIds: Set<MessageId>, 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, *)

View File

@ -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 {

View File

@ -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)
})))
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -47,7 +47,8 @@ final class ChatRecentActionsController: TelegramBaseController {
}, deleteSelectedMessages: {
}, reportSelectedMessages: {
}, reportMessages: { _ in
}, deleteMessages: { _ in
}, deleteMessages: { _, _, f in
f(.default)
}, forwardSelectedMessages: {
}, forwardCurrentForwardMessages: {
}, forwardMessages: { _ in

View File

@ -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<CachedMediaResourceRepresentationResult, NoError> in

View File

@ -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 {

View File

@ -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 = "<group>"; };
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 = "<group>"; };
D09F9DCE20768DAF00DB4DE1 /* SecureIdLocalResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdLocalResource.swift; sourceTree = "<group>"; };
D0A11BF91E7836C20081CE03 /* ChangePhoneNumberIntroController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberIntroController.swift; sourceTree = "<group>"; };
D0A11BFB1E7840750081CE03 /* ChangePhoneNumberController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberController.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */,