mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
First context menu implementation
This commit is contained in:
parent
287dfb86ae
commit
3926531c39
@ -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>
|
||||
|
@ -79,7 +79,6 @@
|
||||
buildConfiguration = "DebugHockeyapp"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
enableASanStackUseAfterReturn = "YES"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
|
@ -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;
|
||||
};
|
||||
|
182
submodules/ContextUI/Sources/ContextActionNode.swift
Normal file
182
submodules/ContextUI/Sources/ContextActionNode.swift
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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) {
|
||||
}
|
||||
}
|
24
submodules/ContextUI/Sources/ContextContentSourceNode.swift
Normal file
24
submodules/ContextUI/Sources/ContextContentSourceNode.swift
Normal 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 {
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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, *)
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
})))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -47,7 +47,8 @@ final class ChatRecentActionsController: TelegramBaseController {
|
||||
}, deleteSelectedMessages: {
|
||||
}, reportSelectedMessages: {
|
||||
}, reportMessages: { _ in
|
||||
}, deleteMessages: { _ in
|
||||
}, deleteMessages: { _, _, f in
|
||||
f(.default)
|
||||
}, forwardSelectedMessages: {
|
||||
}, forwardCurrentForwardMessages: {
|
||||
}, forwardMessages: { _ in
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 */,
|
||||
|
Loading…
x
Reference in New Issue
Block a user