mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Implement text selection in context menu
This commit is contained in:
parent
dd64c3dec1
commit
34c9605060
@ -4481,7 +4481,8 @@ Any member of this group will be able to see messages in the channel.";
|
||||
|
||||
"Chat.SlowmodeTooltip" = "Slowmode is enabled. You can send\nyour next message in %@.";
|
||||
"Chat.SlowmodeTooltipPending" = "Slowmode is enabled. You can't send more than one message at once.";
|
||||
"Chat.AttachmentLimitExceeded" = "Slowmode is enabled. You can't select more items.";
|
||||
"Chat.AttachmentLimitReached" = "You can't select more items.";
|
||||
"Chat.SlowmodeAttachmentLimitReached" = "Slowmode is enabled. You can't select more items.";
|
||||
"Chat.AttachmentMultipleFilesDisabled" = "Slowmode is enabled. You can't send multiple files at once.";
|
||||
"Chat.AttachmentMultipleForwardDisabled" = "Slowmode is enabled. You can't forward multiple messages at once.";
|
||||
"Chat.MultipleTextMessagesDisabled" = "Slowmode is enabled. You can't send multiple messages at once.";
|
||||
@ -4618,3 +4619,5 @@ Any member of this group will be able to see messages in the channel.";
|
||||
"Conversation.SelectedMessages_any" = "%@ Messages Selected";
|
||||
"Conversation.SelectedMessages_many" = "%@ Messages Selected";
|
||||
"Conversation.SelectedMessages_0" = "%@ Messages Selected";
|
||||
|
||||
"GroupInfo.Permissions.SlowmodeValue.Off" = "Off";
|
||||
|
@ -18,6 +18,7 @@
|
||||
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 */; };
|
||||
D0C9CBE42302D45F00FAB518 /* TextSelectionNode.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0C9CBE32302D45F00FAB518 /* TextSelectionNode.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@ -34,6 +35,7 @@
|
||||
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>"; };
|
||||
D0C9CBE32302D45F00FAB518 /* TextSelectionNode.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TextSelectionNode.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -41,6 +43,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D0C9CBE42302D45F00FAB518 /* TextSelectionNode.framework in Frameworks */,
|
||||
D09E777F22F8E47000B9CCA7 /* TelegramPresentationData.framework in Frameworks */,
|
||||
D038AC7922F8A08A00320981 /* AsyncDisplayKit.framework in Frameworks */,
|
||||
D038AC7522F8A06200320981 /* Display.framework in Frameworks */,
|
||||
@ -86,6 +89,7 @@
|
||||
D038AC6F22F8A05A00320981 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0C9CBE32302D45F00FAB518 /* TextSelectionNode.framework */,
|
||||
D09E777E22F8E47000B9CCA7 /* TelegramPresentationData.framework */,
|
||||
D038AC7822F8A08A00320981 /* AsyncDisplayKit.framework */,
|
||||
D038AC7422F8A06200320981 /* Display.framework */,
|
||||
|
@ -6,6 +6,7 @@ public final class ContextContentContainingNode: ASDisplayNode {
|
||||
public let contentNode: ContextContentNode
|
||||
public var contentRect: CGRect = CGRect()
|
||||
public var isExtractedToContextPreview: Bool = false
|
||||
public var willUpdateIsExtractedToContextPreview: ((Bool) -> Void)?
|
||||
public var isExtractedToContextPreviewUpdated: ((Bool) -> Void)?
|
||||
public var updateAbsoluteRect: ((CGRect, CGSize) -> Void)?
|
||||
public var applyAbsoluteOffset: ((CGFloat, ContainedViewLayoutTransitionCurve, Double) -> Void)?
|
||||
|
@ -3,6 +3,7 @@ import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import TextSelectionNode
|
||||
|
||||
public enum ContextMenuActionItemTextLayout {
|
||||
case singleLine
|
||||
@ -115,14 +116,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
|
||||
super.init()
|
||||
|
||||
if #available(iOS 10.0, *) {
|
||||
/*if #available(iOS 10.0, *) {
|
||||
let propertyAnimator = UIViewPropertyAnimator(duration: 0.4, curve: .linear)
|
||||
propertyAnimator.isInterruptible = true
|
||||
propertyAnimator.addAnimations {
|
||||
self.effectView.effect = makeCustomZoomBlurEffect()
|
||||
}
|
||||
self.propertyAnimator = propertyAnimator
|
||||
}
|
||||
}*/
|
||||
|
||||
self.scrollNode.view.delegate = self
|
||||
|
||||
@ -250,7 +251,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
|
||||
self.dimNode.alpha = 1.0
|
||||
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
if let _ = self.propertyAnimator {
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.25, from: 0.0, to: 1.0, update: { [weak self] value in
|
||||
@ -261,50 +262,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
})
|
||||
}
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
UIView.animate(withDuration: 0.2, animations: {
|
||||
self.effectView.effect = makeCustomZoomBlurEffect()
|
||||
})
|
||||
}
|
||||
if #available(iOSApplicationExtension 13.0, iOS 13.0, *) {
|
||||
} else {
|
||||
//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
|
||||
@ -332,11 +293,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
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)
|
||||
self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: putBackInfo.contentAreaInScreenSpace, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false)
|
||||
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
let contentParentNode = self.contentParentNode
|
||||
|
||||
contentParentNode?.willUpdateIsExtractedToContextPreview?(false)
|
||||
|
||||
let intermediateCompletion: () -> Void = { [weak contentParentNode] in
|
||||
if completedEffect && completedContentNode && completedActionsNode {
|
||||
switch result {
|
||||
@ -356,7 +320,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
|
||||
if let propertyAnimator = self.propertyAnimator {
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.22, from: (propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete ?? 0.2, to: 0.0, update: { [weak self] value in
|
||||
self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2, from: (propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete ?? 0.2, to: 0.0, update: { [weak self] value in
|
||||
(self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value
|
||||
}, completion: { [weak self] in
|
||||
self?.effectView.isHidden = true
|
||||
@ -365,7 +329,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
})
|
||||
}
|
||||
} else {
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
UIView.animate(withDuration: 0.2, animations: {
|
||||
if #available(iOS 9.0, *) {
|
||||
self.effectView.effect = nil
|
||||
} else {
|
||||
@ -377,22 +341,22 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
completedActionsNode = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, removeOnCompletion: false)
|
||||
self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, 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)
|
||||
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.2, 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
|
||||
self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentContainerOffset, duration: 0.2, 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)
|
||||
contentParentNode.applyAbsoluteOffset?(-contentContainerOffset.y, .easeInOut, 0.2)
|
||||
} else if let contentParentNode = self.contentParentNode {
|
||||
if let snapshotView = contentParentNode.contentNode.view.snapshotContentTree() {
|
||||
self.contentContainerNode.view.addSubview(snapshotView)
|
||||
@ -402,7 +366,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
contentParentNode.isExtractedToContextPreview = false
|
||||
contentParentNode.isExtractedToContextPreviewUpdated?(false)
|
||||
|
||||
self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
|
||||
self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
completedContentNode = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
@ -520,12 +484,17 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
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)
|
||||
}
|
||||
if let contentParentNode = self.contentParentNode {
|
||||
let contentPoint = self.view.convert(point, to: contentParentNode.contentNode.view)
|
||||
if let result = contentParentNode.contentNode.hitTest(contentPoint, with: event) {
|
||||
if result is TextSelectionNodeView {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self.dimNode.view
|
||||
}
|
||||
@ -587,7 +556,7 @@ public final class ContextController: ViewController {
|
||||
|
||||
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?.dismiss(result: result, completion: nil)
|
||||
}, recognizer: self.recognizer)
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
@ -618,16 +587,17 @@ public final class ContextController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func dismiss(result: ContextMenuActionResult) {
|
||||
private func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) {
|
||||
if !self.wasDismissed {
|
||||
self.wasDismissed = true
|
||||
self.controllerNode.animateOut(result: result, completion: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func dismiss() {
|
||||
self.dismiss(result: .default)
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
self.dismiss(result: .default, completion: completion)
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,11 @@ import AsyncDisplayKit
|
||||
|
||||
public final class ContextMenuControllerPresentationArguments {
|
||||
fileprivate let sourceNodeAndRect: () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?
|
||||
fileprivate let bounce: Bool
|
||||
|
||||
public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?) {
|
||||
public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?, bounce: Bool = true) {
|
||||
self.sourceNodeAndRect = sourceNodeAndRect
|
||||
self.bounce = bounce
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,9 +47,9 @@ public final class ContextMenuController: ViewController, KeyShortcutResponder {
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = ContextMenuNode(actions: self.actions, dismiss: { [weak self] in
|
||||
self?.dismissed?()
|
||||
self?.contextMenuNode.animateOut {
|
||||
self?.contextMenuNode.animateOut(bounce: (self?.presentationArguments as? ContextMenuControllerPresentationArguments)?.bounce ?? true, completion: {
|
||||
self?.presentingViewController?.dismiss(animated: false)
|
||||
}
|
||||
})
|
||||
}, catchTapsOutside: self.catchTapsOutside, hasHapticFeedback: self.hasHapticFeedback)
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
@ -55,14 +57,14 @@ public final class ContextMenuController: ViewController, KeyShortcutResponder {
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.contextMenuNode.animateIn()
|
||||
self.contextMenuNode.animateIn(bounce: (self.presentationArguments as? ContextMenuControllerPresentationArguments)?.bounce ?? true)
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
self.dismissed?()
|
||||
self.contextMenuNode.animateOut { [weak self] in
|
||||
self.contextMenuNode.animateOut(bounce: (self.presentationArguments as? ContextMenuControllerPresentationArguments)?.bounce ?? true, completion: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
@ -70,9 +72,9 @@ public final class ContextMenuController: ViewController, KeyShortcutResponder {
|
||||
|
||||
if self.layout != nil && self.layout! != layout {
|
||||
self.dismissed?()
|
||||
self.contextMenuNode.animateOut { [weak self] in
|
||||
self.contextMenuNode.animateOut(bounce: (self.presentationArguments as? ContextMenuControllerPresentationArguments)?.bounce ?? true, completion: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.layout = layout
|
||||
|
||||
@ -90,7 +92,5 @@ public final class ContextMenuController: ViewController, KeyShortcutResponder {
|
||||
|
||||
override public func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.contextMenuNode.animateIn()
|
||||
}
|
||||
}
|
||||
|
@ -221,21 +221,27 @@ final class ContextMenuNode: ASDisplayNode {
|
||||
self.scrollNode.layout()
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.2)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.4)
|
||||
func animateIn(bounce: Bool) {
|
||||
if bounce {
|
||||
self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.2)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.4)
|
||||
let containerPosition = self.containerNode.layer.position
|
||||
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: containerPosition.x, y: containerPosition.y + (self.arrowOnBottom ? 1.0 : -1.0) * self.containerNode.bounds.size.height / 2.0)), to: NSValue(cgPoint: containerPosition), keyPath: "position", duration: 0.4)
|
||||
}
|
||||
|
||||
let containerPosition = self.containerNode.layer.position
|
||||
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: containerPosition.x, y: containerPosition.y + (self.arrowOnBottom ? 1.0 : -1.0) * self.containerNode.bounds.size.height / 2.0)), to: NSValue(cgPoint: containerPosition), keyPath: "position", duration: 0.4)
|
||||
|
||||
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||
self.containerNode.allowsGroupOpacity = true
|
||||
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak self] _ in
|
||||
self?.containerNode.allowsGroupOpacity = false
|
||||
})
|
||||
|
||||
if let feedback = self.feedback {
|
||||
feedback.impact(.light)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
func animateOut(bounce: Bool, completion: @escaping () -> Void) {
|
||||
self.containerNode.allowsGroupOpacity = true
|
||||
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
self?.containerNode.allowsGroupOpacity = false
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
@ -10,6 +10,9 @@ private enum CornerType {
|
||||
}
|
||||
|
||||
private func drawFullCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
|
||||
if radius.isZero {
|
||||
return
|
||||
}
|
||||
context.setFillColor(color.cgColor)
|
||||
switch type {
|
||||
case .topLeft:
|
||||
|
@ -116,7 +116,7 @@ void testZoomBlurEffect(UIVisualEffect *effect) {
|
||||
UIBlurEffect *makeCustomZoomBlurEffect() {
|
||||
NSString *string = [@[@"_", @"UI", @"Custom", @"BlurEffect"] componentsJoinedByString:@""];
|
||||
CustomBlurEffect *result = (CustomBlurEffect *)[NSClassFromString(string) effectWithStyle:0];
|
||||
result.blurRadius = 7.0;
|
||||
result.blurRadius = 9.0;
|
||||
result.zoom = 0.015;
|
||||
result.colorTint = nil;
|
||||
result.colorTintAlpha = 0.0;
|
||||
|
@ -1173,7 +1173,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
|
||||
TGMediaSelectionContext *selectionContext = _selectionContext;
|
||||
if (selectionContext == nil)
|
||||
{
|
||||
selectionContext = [[TGMediaSelectionContext alloc] initWithGroupingAllowed:self.allowGrouping selectionLimit:30];
|
||||
selectionContext = [[TGMediaSelectionContext alloc] initWithGroupingAllowed:self.allowGrouping selectionLimit:100];
|
||||
if (self.allowGrouping)
|
||||
selectionContext.grouping = true;
|
||||
_selectionContext = selectionContext;
|
||||
|
@ -60,7 +60,7 @@ const CGFloat TGClipboardPreviewEdgeInset = 8.0f;
|
||||
[_collectionView registerClass:[TGClipboardPreviewCell class] forCellWithReuseIdentifier:TGClipboardPreviewCellIdentifier];
|
||||
[self addSubview:_collectionView];
|
||||
|
||||
_selectionContext = [[TGMediaSelectionContext alloc] initWithGroupingAllowed:false selectionLimit:30];
|
||||
_selectionContext = [[TGMediaSelectionContext alloc] initWithGroupingAllowed:false selectionLimit:100];
|
||||
|
||||
for (UIImage *image in _images)
|
||||
{
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
return [self initWithGroupingAllowed:false selectionLimit:30];
|
||||
return [self initWithGroupingAllowed:false selectionLimit:100];
|
||||
}
|
||||
|
||||
- (instancetype)initWithGroupingAllowed:(bool)allowGrouping selectionLimit:(int)selectionLimit
|
||||
|
@ -51,7 +51,7 @@
|
||||
|
||||
__weak TGMenuSheetController *weakController = controller;
|
||||
__weak TGViewController *weakParentController = parentController;
|
||||
TGAttachmentCarouselItemView *carouselItem = [[TGAttachmentCarouselItemView alloc] initWithContext:context camera:true selfPortrait:intent == TGPassportAttachIntentSelfie forProfilePhoto:false assetType:TGMediaAssetPhotoType saveEditedPhotos:false allowGrouping:false allowSelection:intent == TGPassportAttachIntentMultiple allowEditing:true document:true selectionLimit: 10];
|
||||
TGAttachmentCarouselItemView *carouselItem = [[TGAttachmentCarouselItemView alloc] initWithContext:context camera:true selfPortrait:intent == TGPassportAttachIntentSelfie forProfilePhoto:false assetType:TGMediaAssetPhotoType saveEditedPhotos:false allowGrouping:false allowSelection:intent == TGPassportAttachIntentMultiple allowEditing:true document:true selectionLimit:10];
|
||||
__weak TGAttachmentCarouselItemView *weakCarouselItem = carouselItem;
|
||||
carouselItem.onlyCrop = true;
|
||||
carouselItem.parentController = parentController;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import TelegramCore
|
||||
|
||||
public enum MessageBubbleImageNeighbors {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -257,6 +257,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
private weak var slowmodeTooltipController: ChatSlowmodeHintController?
|
||||
|
||||
private weak var currentContextController: ContextController?
|
||||
|
||||
private weak var sendMessageActionsController: ChatSendMessageActionSheetController?
|
||||
|
||||
private var screenCaptureEventsDisposable: Disposable?
|
||||
@ -545,7 +547,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
f(.dismissWithoutContent)
|
||||
})), at: 0)
|
||||
}
|
||||
strongSelf.window?.presentInGlobalOverlay(ContextController(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: ChatMessageContextControllerContentSource(chatNode: strongSelf.chatDisplayNode, message: message), items: actions, recognizer: recognizer))
|
||||
let controller = ContextController(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: ChatMessageContextControllerContentSource(chatNode: strongSelf.chatDisplayNode, message: message), items: actions, recognizer: recognizer)
|
||||
strongSelf.currentContextController = controller
|
||||
strongSelf.window?.presentInGlobalOverlay(controller)
|
||||
})
|
||||
}
|
||||
}, navigateToMessage: { [weak self] fromId, id in
|
||||
@ -1004,6 +1008,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
self?.present(controller, in: .window(.root), with: arguments)
|
||||
}, navigationController: { [weak self] in
|
||||
return self?.navigationController as? NavigationController
|
||||
}, chatControllerNode: { [weak self] in
|
||||
return self?.chatDisplayNode
|
||||
}, presentGlobalOverlayController: { [weak self] controller, arguments in
|
||||
self?.presentInGlobalOverlay(controller, with: arguments)
|
||||
}, callPeer: { [weak self] peerId in
|
||||
@ -1485,6 +1491,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
strongSelf.present(controller, in: .window(.root))
|
||||
}
|
||||
}, performTextSelectionAction: { [weak self] _, text, action in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
switch action {
|
||||
case .copy:
|
||||
UIPasteboard.general.string = text
|
||||
case .share:
|
||||
let f = {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let shareController = ShareController(context: strongSelf.context, subject: .text(text), externalShare: true, immediateExternalShare: false)
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
strongSelf.present(shareController, in: .window(.root))
|
||||
}
|
||||
if let currentContextController = strongSelf.currentContextController {
|
||||
currentContextController.dismiss(completion: {
|
||||
f()
|
||||
})
|
||||
} else {
|
||||
f()
|
||||
}
|
||||
case .lookup:
|
||||
let controller = UIReferenceLibraryViewController(term: text)
|
||||
if let window = strongSelf.navigationController?.view.window {
|
||||
controller.popoverPresentationController?.sourceView = window
|
||||
controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
|
||||
window.rootViewController?.present(controller, animated: true)
|
||||
}
|
||||
}
|
||||
}, requestMessageUpdate: { [weak self] id in
|
||||
if let strongSelf = self {
|
||||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
|
||||
@ -4818,7 +4855,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentLimitExceeded, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentLimitReached, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, presentCantSendMultipleFiles: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -4950,7 +4987,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return
|
||||
}
|
||||
let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText
|
||||
var selectionLimit: Int = 30
|
||||
var selectionLimit: Int = 100
|
||||
if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode {
|
||||
selectionLimit = 10
|
||||
}
|
||||
@ -4988,7 +5025,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentLimitExceeded, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentLimitReached, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
})
|
||||
controller.descriptionGenerator = legacyAssetPickerItemGenerator()
|
||||
controller.completionBlock = { [weak legacyController] signals, silentPosting in
|
||||
|
@ -6,6 +6,7 @@ import TelegramCore
|
||||
import Display
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import TextSelectionNode
|
||||
|
||||
struct ChatInterfaceHighlightedState: Equatable {
|
||||
let messageStableId: UInt32
|
||||
@ -71,6 +72,7 @@ public final class ChatControllerInteraction {
|
||||
let openMessageShareMenu: (MessageId) -> Void
|
||||
let presentController: (ViewController, Any?) -> Void
|
||||
let navigationController: () -> NavigationController?
|
||||
let chatControllerNode: () -> ASDisplayNode?
|
||||
let presentGlobalOverlayController: (ViewController, Any?) -> Void
|
||||
let callPeer: (PeerId) -> Void
|
||||
let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void
|
||||
@ -89,6 +91,7 @@ public final class ChatControllerInteraction {
|
||||
let scheduleCurrentMessage: () -> Void
|
||||
let sendScheduledMessagesNow: ([MessageId]) -> Void
|
||||
let editScheduledMessagesTime: ([MessageId]) -> Void
|
||||
let performTextSelectionAction: (UInt32, String, TextSelectionAction) -> Void
|
||||
|
||||
let requestMessageUpdate: (MessageId) -> Void
|
||||
let cancelInteractiveKeyboardGestures: () -> Void
|
||||
@ -103,7 +106,7 @@ public final class ChatControllerInteraction {
|
||||
var searchTextHighightState: String?
|
||||
var seenOneTimeAnimatedMedia = Set<MessageId>()
|
||||
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
self.openMessage = openMessage
|
||||
self.openPeer = openPeer
|
||||
self.openPeerMention = openPeerMention
|
||||
@ -130,6 +133,7 @@ public final class ChatControllerInteraction {
|
||||
self.openMessageShareMenu = openMessageShareMenu
|
||||
self.presentController = presentController
|
||||
self.navigationController = navigationController
|
||||
self.chatControllerNode = chatControllerNode
|
||||
self.presentGlobalOverlayController = presentGlobalOverlayController
|
||||
self.callPeer = callPeer
|
||||
self.longTap = longTap
|
||||
@ -148,6 +152,7 @@ public final class ChatControllerInteraction {
|
||||
self.scheduleCurrentMessage = scheduleCurrentMessage
|
||||
self.sendScheduledMessagesNow = sendScheduledMessagesNow
|
||||
self.editScheduledMessagesTime = editScheduledMessagesTime
|
||||
self.performTextSelectionAction = performTextSelectionAction
|
||||
|
||||
self.requestMessageUpdate = requestMessageUpdate
|
||||
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
|
||||
@ -163,6 +168,8 @@ public final class ChatControllerInteraction {
|
||||
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in }, navigationController: {
|
||||
return nil
|
||||
}, chatControllerNode: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
|
||||
}, canSetupReply: { _ in
|
||||
return false
|
||||
@ -177,6 +184,7 @@ public final class ChatControllerInteraction {
|
||||
}, scheduleCurrentMessage: {
|
||||
}, sendScheduledMessagesNow: { _ in
|
||||
}, editScheduledMessagesTime: { _ in
|
||||
}, performTextSelectionAction: { _, _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
@ -166,4 +166,10 @@ class ChatMessageBubbleContentNode: ASDisplayNode {
|
||||
func updateHighlightedState(animated: Bool) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func willUpdateIsExtractedToContextPreview(_ value: Bool) {
|
||||
}
|
||||
|
||||
func updateIsExtractedToContextPreview(_ value: Bool) {
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import LocalizedPeerData
|
||||
import ContextUI
|
||||
import TelegramUniversalVideoContent
|
||||
import MosaicLayout
|
||||
import TextSelectionNode
|
||||
|
||||
private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(Message, AnyClass)] {
|
||||
var result: [(Message, AnyClass)] = []
|
||||
@ -167,6 +168,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
private var appliedItem: ChatMessageItem?
|
||||
private var appliedForwardInfo: (Peer?, String?)?
|
||||
|
||||
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||
|
||||
override var visibility: ListViewItemNodeVisibility {
|
||||
didSet {
|
||||
if self.visibility != oldValue {
|
||||
@ -205,6 +208,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
self?.accessibilityElementDidBecomeFocused()
|
||||
}
|
||||
|
||||
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtractedToContextPreview in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
for contentNode in strongSelf.contentNodes {
|
||||
contentNode.willUpdateIsExtractedToContextPreview(isExtractedToContextPreview)
|
||||
}
|
||||
}
|
||||
self.contextSourceNode.isExtractedToContextPreviewUpdated = { [weak self] isExtractedToContextPreview in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
@ -214,6 +225,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
if !isExtractedToContextPreview, let (rect, size) = strongSelf.absoluteRect {
|
||||
strongSelf.updateAbsoluteRect(rect, within: size)
|
||||
}
|
||||
|
||||
for contentNode in strongSelf.contentNodes {
|
||||
contentNode.updateIsExtractedToContextPreview(isExtractedToContextPreview)
|
||||
}
|
||||
}
|
||||
|
||||
self.contextSourceNode.updateAbsoluteRect = { [weak self] rect, size in
|
||||
@ -338,6 +353,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tapRecognizer = recognizer
|
||||
self.view.addGestureRecognizer(recognizer)
|
||||
|
||||
let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:)))
|
||||
@ -1569,6 +1585,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
strongSelf.contextSourceNode.contentNode.addSubnode(contentNode)
|
||||
|
||||
contentNode.visibility = strongSelf.visibility
|
||||
contentNode.updateIsExtractedToContextPreview(strongSelf.contextSourceNode.isExtractedToContextPreview)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2185,6 +2202,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
return nil
|
||||
}
|
||||
|
||||
if self.contextSourceNode.isExtractedToContextPreview {
|
||||
if let result = super.hitTest(point, with: event) as? TextSelectionNodeView {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) {
|
||||
return shareButtonNode.view
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import Postbox
|
||||
import TextFormat
|
||||
import UrlEscaping
|
||||
import TelegramUniversalVideoContent
|
||||
import TextSelectionNode
|
||||
|
||||
private final class CachedChatMessageText {
|
||||
let text: String
|
||||
@ -39,6 +40,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode
|
||||
private let statusNode: ChatMessageDateAndStatusNode
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
private var textSelectionNode: TextSelectionNode?
|
||||
|
||||
private var textHighlightingNodes: [LinkHighlightingNode] = []
|
||||
|
||||
@ -504,4 +506,38 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.textHighlightingNodes.remove(at: i)
|
||||
}
|
||||
}
|
||||
|
||||
override func willUpdateIsExtractedToContextPreview(_ value: Bool) {
|
||||
if !value {
|
||||
if let textSelectionNode = self.textSelectionNode {
|
||||
self.textSelectionNode = nil
|
||||
textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in
|
||||
textSelectionNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func updateIsExtractedToContextPreview(_ value: Bool) {
|
||||
if value {
|
||||
if self.textSelectionNode == nil, let item = self.item, let rootNode = item.controllerInteraction.chatControllerNode() {
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: item.presentationData.theme.theme.list.itemAccentColor.withAlphaComponent(0.5), knob: item.presentationData.theme.theme.list.itemAccentColor), textNode: self.textNode, present: { [weak self] c, a in
|
||||
self?.item?.controllerInteraction.presentGlobalOverlayController(c, a)
|
||||
}, rootNode: rootNode, performAction: { [weak self] text, action in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.performTextSelectionAction(item.message.stableId, text, action)
|
||||
})
|
||||
self.textSelectionNode = textSelectionNode
|
||||
self.addSubnode(textSelectionNode)
|
||||
textSelectionNode.frame = self.textNode.frame
|
||||
}
|
||||
} else if let textSelectionNode = self.textSelectionNode {
|
||||
self.textSelectionNode = nil
|
||||
textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in
|
||||
textSelectionNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -229,6 +229,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, presentController: { _, _ in
|
||||
}, navigationController: { [weak self] in
|
||||
return self?.getNavigationController()
|
||||
}, chatControllerNode: { [weak self] in
|
||||
return self
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action, message in
|
||||
if let strongSelf = self {
|
||||
switch action {
|
||||
@ -400,6 +402,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, scheduleCurrentMessage: {
|
||||
}, sendScheduledMessagesNow: { _ in
|
||||
}, editScheduledMessagesTime: { _ in
|
||||
}, performTextSelectionAction: { _, _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||
|
@ -188,7 +188,7 @@ class ChatSlowmodeItemNode: ListViewItemNode {
|
||||
|
||||
let valueString: String
|
||||
if value == 0 {
|
||||
valueString = item.strings.Profile_MessageLifetimeForever
|
||||
valueString = item.strings.GroupInfo_Permissions_SlowmodeValue_Off
|
||||
} else {
|
||||
valueString = shortTimeIntervalString(strings: item.strings, value: value)
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaOptions:
|
||||
|
||||
var underlyingViews: [UIView] = []
|
||||
|
||||
var selectionLimit: Int32 = 30
|
||||
var selectionLimit: Int32 = 100
|
||||
var slowModeEnabled = false
|
||||
if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode {
|
||||
slowModeEnabled = true
|
||||
|
@ -88,6 +88,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
}, presentController: { _, _ in
|
||||
}, navigationController: {
|
||||
return nil
|
||||
}, chatControllerNode: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in
|
||||
}, callPeer: { _ in
|
||||
}, longTap: { _, _ in
|
||||
@ -107,6 +109,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
}, scheduleCurrentMessage: {
|
||||
}, sendScheduledMessagesNow: { _ in
|
||||
}, editScheduledMessagesTime: { _ in
|
||||
}, performTextSelectionAction: { _, _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false))
|
||||
|
@ -216,6 +216,8 @@ public class PeerMediaCollectionController: TelegramBaseController {
|
||||
}, presentController: { _, _ in
|
||||
}, navigationController: {
|
||||
return nil
|
||||
}, chatControllerNode: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in
|
||||
}, longTap: { [weak self] content, _ in
|
||||
if let strongSelf = self {
|
||||
@ -280,6 +282,7 @@ public class PeerMediaCollectionController: TelegramBaseController {
|
||||
}, scheduleCurrentMessage: {
|
||||
}, sendScheduledMessagesNow: { _ in
|
||||
}, editScheduledMessagesTime: { _ in
|
||||
}, performTextSelectionAction: { _, _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
Binary file not shown.
407
submodules/TextSelectionNode/Sources/TextSelectionNode.swift
Normal file
407
submodules/TextSelectionNode/Sources/TextSelectionNode.swift
Normal file
@ -0,0 +1,407 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import UIKit.UIGestureRecognizerSubclass
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
private func findScrollView(view: UIView?) -> UIScrollView? {
|
||||
if let view = view {
|
||||
if let view = view as? UIScrollView {
|
||||
return view
|
||||
}
|
||||
return findScrollView(view: view.superview)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelScrollViewGestures(view: UIView?) {
|
||||
if let view = view {
|
||||
if let gestureRecognizers = view.gestureRecognizers {
|
||||
for recognizer in gestureRecognizers {
|
||||
if let recognizer = recognizer as? UIPanGestureRecognizer {
|
||||
switch recognizer.state {
|
||||
case .began, .possible:
|
||||
recognizer.state = .ended
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cancelScrollViewGestures(view: view.superview)
|
||||
}
|
||||
}
|
||||
|
||||
private func generateKnobImage(color: UIColor, inverted: Bool = false) -> UIImage? {
|
||||
let f: (CGSize, CGContext) -> Void = { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(x: (size.width - 2.0) / 2.0, y: size.width / 2.0), size: CGSize(width: 2.0, height: size.height - size.width / 2.0 - 1.0)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: (size.width - 2.0) / 2.0, y: size.width + 2.0), size: CGSize(width: 2.0, height: 2.0)))
|
||||
}
|
||||
let size = CGSize(width: 12.0, height: 12.0 + 2.0 + 2.0)
|
||||
if inverted {
|
||||
return generateImage(size, contextGenerator: f)?.stretchableImage(withLeftCapWidth: Int(size.width / 2.0), topCapHeight: Int(size.height) - (Int(size.width) + 1))
|
||||
} else {
|
||||
return generateImage(size, rotatedContext: f)?.stretchableImage(withLeftCapWidth: Int(size.width / 2.0), topCapHeight: Int(size.width) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
public final class TextSelectionTheme {
|
||||
public let selection: UIColor
|
||||
public let knob: UIColor
|
||||
|
||||
public init(selection: UIColor, knob: UIColor) {
|
||||
self.selection = selection
|
||||
self.knob = knob
|
||||
}
|
||||
}
|
||||
|
||||
private enum Knob {
|
||||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
private final class TextSelectionGetureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
|
||||
private var longTapTimer: Timer?
|
||||
private var movingKnob: (Knob, CGPoint, CGPoint)?
|
||||
private var currentLocation: CGPoint?
|
||||
|
||||
var beginSelection: ((CGPoint) -> Void)?
|
||||
var knobAtPoint: ((CGPoint) -> (Knob, CGPoint)?)?
|
||||
var moveKnob: ((Knob, CGPoint) -> Void)?
|
||||
var finishedMovingKnob: (() -> Void)?
|
||||
var clearSelection: (() -> Void)?
|
||||
|
||||
override init(target: Any?, action: Selector?) {
|
||||
super.init(target: nil, action: nil)
|
||||
|
||||
self.delegate = self
|
||||
}
|
||||
|
||||
override public func reset() {
|
||||
super.reset()
|
||||
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
|
||||
self.movingKnob = nil
|
||||
self.currentLocation = nil
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
let currentLocation = touches.first?.location(in: self.view)
|
||||
self.currentLocation = currentLocation
|
||||
|
||||
if let currentLocation = currentLocation {
|
||||
if let (knob, knobPosition) = self.knobAtPoint?(currentLocation) {
|
||||
self.movingKnob = (knob, knobPosition, currentLocation)
|
||||
cancelScrollViewGestures(view: self.view?.superview)
|
||||
self.state = .began
|
||||
} else if self.longTapTimer == nil {
|
||||
final class TimerTarget: NSObject {
|
||||
let f: () -> Void
|
||||
|
||||
init(_ f: @escaping () -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
@objc func event() {
|
||||
self.f()
|
||||
}
|
||||
}
|
||||
let longTapTimer = Timer(timeInterval: 0.3, target: TimerTarget({ [weak self] in
|
||||
self?.longTapEvent()
|
||||
}), selector: #selector(TimerTarget.event), userInfo: nil, repeats: false)
|
||||
self.longTapTimer = longTapTimer
|
||||
RunLoop.main.add(longTapTimer, forMode: .common)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
let currentLocation = touches.first?.location(in: self.view)
|
||||
self.currentLocation = currentLocation
|
||||
|
||||
if let (knob, initialKnobPosition, initialGesturePosition) = self.movingKnob, let currentLocation = currentLocation {
|
||||
|
||||
self.moveKnob?(knob, CGPoint(x: initialKnobPosition.x + currentLocation.x - initialGesturePosition.x, y: initialKnobPosition.y + currentLocation.y - initialGesturePosition.y))
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
if let longTapTimer = self.longTapTimer {
|
||||
self.longTapTimer = nil
|
||||
longTapTimer.invalidate()
|
||||
self.clearSelection?()
|
||||
} else {
|
||||
if let _ = self.currentLocation, let _ = self.movingKnob {
|
||||
self.finishedMovingKnob?()
|
||||
}
|
||||
}
|
||||
self.state = .ended
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.state = .cancelled
|
||||
}
|
||||
|
||||
private func longTapEvent() {
|
||||
if let currentLocation = self.currentLocation {
|
||||
self.beginSelection?(currentLocation)
|
||||
self.state = .ended
|
||||
}
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class TextSelectionNodeView: UIView {
|
||||
|
||||
}
|
||||
|
||||
public enum TextSelectionAction {
|
||||
case copy
|
||||
case share
|
||||
case lookup
|
||||
}
|
||||
|
||||
public final class TextSelectionNode: ASDisplayNode {
|
||||
private let theme: TextSelectionTheme
|
||||
private let textNode: TextNode
|
||||
private let present: (ViewController, Any?) -> Void
|
||||
private weak var rootNode: ASDisplayNode?
|
||||
private let performAction: (String, TextSelectionAction) -> Void
|
||||
private var highlightOverlay: LinkHighlightingNode?
|
||||
private let leftKnob: ASImageNode
|
||||
private let rightKnob: ASImageNode
|
||||
|
||||
private var currentRange: (Int, Int)?
|
||||
private var currentRects: [CGRect]?
|
||||
|
||||
public init(theme: TextSelectionTheme, textNode: TextNode, present: @escaping (ViewController, Any?) -> Void, rootNode: ASDisplayNode, performAction: @escaping (String, TextSelectionAction) -> Void) {
|
||||
self.theme = theme
|
||||
self.textNode = textNode
|
||||
self.present = present
|
||||
self.rootNode = rootNode
|
||||
self.performAction = performAction
|
||||
self.leftKnob = ASImageNode()
|
||||
self.leftKnob.isUserInteractionEnabled = false
|
||||
self.leftKnob.image = generateKnobImage(color: theme.knob)
|
||||
self.leftKnob.displaysAsynchronously = false
|
||||
self.leftKnob.displayWithoutProcessing = true
|
||||
self.leftKnob.alpha = 0.0
|
||||
self.rightKnob = ASImageNode()
|
||||
self.rightKnob.isUserInteractionEnabled = false
|
||||
self.rightKnob.image = generateKnobImage(color: theme.knob, inverted: true)
|
||||
self.rightKnob.displaysAsynchronously = false
|
||||
self.rightKnob.displayWithoutProcessing = true
|
||||
self.rightKnob.alpha = 0.0
|
||||
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return TextSelectionNodeView()
|
||||
})
|
||||
|
||||
self.addSubnode(self.leftKnob)
|
||||
self.addSubnode(self.rightKnob)
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let recognizer = TextSelectionGetureRecognizer(target: nil, action: nil)
|
||||
recognizer.knobAtPoint = { [weak self] point in
|
||||
guard let strongSelf = self else {
|
||||
return nil
|
||||
}
|
||||
if !strongSelf.leftKnob.alpha.isZero, strongSelf.leftKnob.frame.insetBy(dx: -4.0, dy: -8.0).contains(point) {
|
||||
return (.left, strongSelf.leftKnob.frame.offsetBy(dx: 0.0, dy: strongSelf.leftKnob.frame.width / 2.0).center)
|
||||
}
|
||||
if !strongSelf.rightKnob.alpha.isZero, strongSelf.rightKnob.frame.insetBy(dx: -4.0, dy: -8.0).contains(point) {
|
||||
return (.right, strongSelf.rightKnob.frame.offsetBy(dx: 0.0, dy: -strongSelf.rightKnob.frame.width / 2.0).center)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
recognizer.moveKnob = { [weak self] knob, point in
|
||||
guard let strongSelf = self, let cachedLayout = strongSelf.textNode.cachedLayout, let _ = cachedLayout.attributedString, let currentRange = strongSelf.currentRange else {
|
||||
return
|
||||
}
|
||||
|
||||
let mappedPoint = strongSelf.view.convert(point, to: strongSelf.textNode.view)
|
||||
if let stringIndex = strongSelf.textNode.attributesAtPoint(mappedPoint, orNearest: true)?.0 {
|
||||
//let string = attributedString.string as NSString
|
||||
var updatedMin = currentRange.0
|
||||
var updatedMax = currentRange.1
|
||||
switch knob {
|
||||
case .left:
|
||||
updatedMin = stringIndex
|
||||
case .right:
|
||||
updatedMax = stringIndex
|
||||
}
|
||||
let updatedRange = NSRange(location: min(updatedMin, updatedMax), length: max(updatedMin, updatedMax) - min(updatedMin, updatedMax))
|
||||
if strongSelf.currentRange?.0 != updatedMin || strongSelf.currentRange?.1 != updatedMax {
|
||||
strongSelf.currentRange = (updatedMin, updatedMax)
|
||||
strongSelf.updateSelection(range: updatedRange)
|
||||
}
|
||||
|
||||
if let scrollView = findScrollView(view: strongSelf.view) {
|
||||
let scrollPoint = strongSelf.view.convert(point, to: scrollView)
|
||||
scrollView.scrollRectToVisible(CGRect(origin: CGPoint(x: scrollPoint.x, y: scrollPoint.y - 30.0), size: CGSize(width: 1.0, height: 60.0)), animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
recognizer.finishedMovingKnob = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.displayMenu()
|
||||
}
|
||||
recognizer.beginSelection = { [weak self] point in
|
||||
guard let strongSelf = self, let cachedLayout = strongSelf.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.dismissSelection()
|
||||
|
||||
let mappedPoint = strongSelf.view.convert(point, to: strongSelf.textNode.view)
|
||||
var resultRange: NSRange?
|
||||
if let stringIndex = strongSelf.textNode.attributesAtPoint(mappedPoint, orNearest: false)?.0 {
|
||||
let string = attributedString.string as NSString
|
||||
|
||||
let inputRange = CFRangeMake(0, string.length)
|
||||
let flag = UInt(kCFStringTokenizerUnitWord)
|
||||
let locale = CFLocaleCopyCurrent()
|
||||
let tokenizer = CFStringTokenizerCreate( kCFAllocatorDefault, string as CFString, inputRange, flag, locale)
|
||||
var tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer)
|
||||
|
||||
while !tokenType.isEmpty {
|
||||
let currentTokenRange = CFStringTokenizerGetCurrentTokenRange(tokenizer)
|
||||
if currentTokenRange.location <= stringIndex && currentTokenRange.location + currentTokenRange.length > stringIndex {
|
||||
resultRange = NSRange(location: currentTokenRange.location, length: currentTokenRange.length)
|
||||
break
|
||||
}
|
||||
tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer)
|
||||
}
|
||||
if resultRange == nil {
|
||||
resultRange = NSRange(location: stringIndex, length: 1)
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.currentRange = resultRange.flatMap {
|
||||
($0.lowerBound, $0.upperBound)
|
||||
}
|
||||
strongSelf.updateSelection(range: resultRange)
|
||||
strongSelf.displayMenu()
|
||||
}
|
||||
recognizer.clearSelection = { [weak self] in
|
||||
self?.dismissSelection()
|
||||
}
|
||||
self.view.addGestureRecognizer(recognizer)
|
||||
}
|
||||
|
||||
private func updateSelection(range: NSRange?) {
|
||||
var rects: [CGRect]?
|
||||
|
||||
if let range = range {
|
||||
rects = self.textNode.rangeRects(in: range)
|
||||
}
|
||||
|
||||
self.currentRects = rects
|
||||
|
||||
if let rects = rects, !rects.isEmpty {
|
||||
let highlightOverlay: LinkHighlightingNode
|
||||
if let current = self.highlightOverlay {
|
||||
highlightOverlay = current
|
||||
} else {
|
||||
highlightOverlay = LinkHighlightingNode(color: self.theme.selection)
|
||||
highlightOverlay.isUserInteractionEnabled = false
|
||||
highlightOverlay.innerRadius = 0.0
|
||||
highlightOverlay.outerRadius = 0.0
|
||||
highlightOverlay.inset = 1.0
|
||||
self.highlightOverlay = highlightOverlay
|
||||
self.insertSubnode(highlightOverlay, at: 0)
|
||||
}
|
||||
highlightOverlay.frame = self.bounds
|
||||
highlightOverlay.updateRects(rects)
|
||||
if let image = self.leftKnob.image {
|
||||
self.leftKnob.frame = CGRect(origin: CGPoint(x: floor(rects[0].minX - 1.0 - image.size.width / 2.0), y: rects[0].minY - 1.0 - image.size.width), size: CGSize(width: image.size.width, height: image.size.width + rects[0].height + 2.0))
|
||||
self.rightKnob.frame = CGRect(origin: CGPoint(x: floor(rects[rects.count - 1].maxX + 1.0 - image.size.width / 2.0), y: rects[rects.count - 1].maxY + 1.0 - (rects[0].height + 2.0)), size: CGSize(width: image.size.width, height: image.size.width + rects[0].height + 2.0))
|
||||
}
|
||||
if self.leftKnob.alpha.isZero {
|
||||
highlightOverlay.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
||||
self.leftKnob.alpha = 1.0
|
||||
self.leftKnob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
||||
self.rightKnob.alpha = 1.0
|
||||
self.rightKnob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
||||
}
|
||||
} else if let highlightOverlay = self.highlightOverlay {
|
||||
self.highlightOverlay = nil
|
||||
highlightOverlay.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak highlightOverlay] _ in
|
||||
highlightOverlay?.removeFromSupernode()
|
||||
})
|
||||
self.leftKnob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
||||
self.leftKnob.alpha = 0.0
|
||||
self.leftKnob.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18)
|
||||
self.rightKnob.alpha = 0.0
|
||||
self.rightKnob.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18)
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissSelection() {
|
||||
self.currentRange = nil
|
||||
self.updateSelection(range: nil)
|
||||
}
|
||||
|
||||
private func displayMenu() {
|
||||
guard let currentRects = self.currentRects, !currentRects.isEmpty, let currentRange = self.currentRange, let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else {
|
||||
return
|
||||
}
|
||||
let range = NSRange(location: min(currentRange.0, currentRange.1), length: max(currentRange.0, currentRange.1) - min(currentRange.0, currentRange.1))
|
||||
var completeRect = currentRects[0]
|
||||
for i in 0 ..< currentRects.count {
|
||||
completeRect = completeRect.union(currentRects[i])
|
||||
}
|
||||
completeRect = completeRect.insetBy(dx: 0.0, dy: -12.0)
|
||||
|
||||
let text = (attributedString.string as NSString).substring(with: range)
|
||||
|
||||
var actions: [ContextMenuAction] = []
|
||||
actions.append(ContextMenuAction(content: .text(title: "Copy", accessibilityLabel: "Copy"), action: { [weak self] in
|
||||
self?.performAction(text, .copy)
|
||||
self?.dismissSelection()
|
||||
}))
|
||||
actions.append(ContextMenuAction(content: .text(title: "Look Up", accessibilityLabel: "Look Up"), action: { [weak self] in
|
||||
self?.performAction(text, .lookup)
|
||||
self?.dismissSelection()
|
||||
}))
|
||||
actions.append(ContextMenuAction(content: .text(title: "Share...", accessibilityLabel: "Share"), action: { [weak self] in
|
||||
self?.performAction(text, .share)
|
||||
self?.dismissSelection()
|
||||
}))
|
||||
self.present(ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||||
guard let strongSelf = self, let rootNode = strongSelf.rootNode else {
|
||||
return nil
|
||||
}
|
||||
return (strongSelf, completeRect, rootNode, rootNode.bounds)
|
||||
}, bounce: false))
|
||||
}
|
||||
}
|
@ -8,12 +8,22 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
D0C9CBCC2302C00600FAB518 /* TextSelectionNode.h in Headers */ = {isa = PBXBuildFile; fileRef = D0C9CBCA2302C00600FAB518 /* TextSelectionNode.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
D0C9CBD92302C2E600FAB518 /* TextSelectionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C9CBD82302C2E600FAB518 /* TextSelectionNode.swift */; };
|
||||
D0C9CBDC2302C31100FAB518 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0C9CBDB2302C31100FAB518 /* Foundation.framework */; };
|
||||
D0C9CBDE2302C31500FAB518 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0C9CBDD2302C31500FAB518 /* UIKit.framework */; };
|
||||
D0C9CBE02302C31800FAB518 /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0C9CBDF2302C31800FAB518 /* AsyncDisplayKit.framework */; };
|
||||
D0C9CBE22302C31D00FAB518 /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0C9CBE12302C31D00FAB518 /* Display.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
D0C9CBC72302C00600FAB518 /* TextSelectionNode.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TextSelectionNode.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D0C9CBCA2302C00600FAB518 /* TextSelectionNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TextSelectionNode.h; sourceTree = "<group>"; };
|
||||
D0C9CBCB2302C00600FAB518 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D0C9CBD82302C2E600FAB518 /* TextSelectionNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextSelectionNode.swift; sourceTree = "<group>"; };
|
||||
D0C9CBDB2302C31100FAB518 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
|
||||
D0C9CBDD2302C31500FAB518 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
|
||||
D0C9CBDF2302C31800FAB518 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D0C9CBE12302C31D00FAB518 /* Display.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Display.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -21,6 +31,10 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D0C9CBE22302C31D00FAB518 /* Display.framework in Frameworks */,
|
||||
D0C9CBE02302C31800FAB518 /* AsyncDisplayKit.framework in Frameworks */,
|
||||
D0C9CBDE2302C31500FAB518 /* UIKit.framework in Frameworks */,
|
||||
D0C9CBDC2302C31100FAB518 /* Foundation.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -30,8 +44,10 @@
|
||||
D0C9CBBD2302C00600FAB518 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0C9CBC92302C00600FAB518 /* TextSelectionNode */,
|
||||
D0C9CBCB2302C00600FAB518 /* Info.plist */,
|
||||
D0C9CBC92302C00600FAB518 /* Sources */,
|
||||
D0C9CBC82302C00600FAB518 /* Products */,
|
||||
D0C9CBDA2302C30E00FAB518 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@ -43,13 +59,24 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0C9CBC92302C00600FAB518 /* TextSelectionNode */ = {
|
||||
D0C9CBC92302C00600FAB518 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0C9CBD82302C2E600FAB518 /* TextSelectionNode.swift */,
|
||||
D0C9CBCA2302C00600FAB518 /* TextSelectionNode.h */,
|
||||
D0C9CBCB2302C00600FAB518 /* Info.plist */,
|
||||
);
|
||||
path = TextSelectionNode;
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0C9CBDA2302C30E00FAB518 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0C9CBE12302C31D00FAB518 /* Display.framework */,
|
||||
D0C9CBDF2302C31800FAB518 /* AsyncDisplayKit.framework */,
|
||||
D0C9CBDD2302C31500FAB518 /* UIKit.framework */,
|
||||
D0C9CBDB2302C31100FAB518 /* Foundation.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
@ -96,6 +123,7 @@
|
||||
TargetAttributes = {
|
||||
D0C9CBC62302C00600FAB518 = {
|
||||
CreatedOnToolsVersion = 10.3;
|
||||
LastSwiftMigration = 1030;
|
||||
};
|
||||
};
|
||||
};
|
||||
@ -131,6 +159,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D0C9CBD92302C2E600FAB518 /* TextSelectionNode.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -262,6 +291,7 @@
|
||||
D0C9CBD02302C00600FAB518 /* DebugAppStoreLLC */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEFINES_MODULE = YES;
|
||||
@ -281,6 +311,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@ -289,6 +320,7 @@
|
||||
D0C9CBD12302C00600FAB518 /* ReleaseAppStoreLLC */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEFINES_MODULE = YES;
|
||||
@ -380,6 +412,7 @@
|
||||
D0C9CBD32302C06100FAB518 /* DebugHockeyapp */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEFINES_MODULE = YES;
|
||||
@ -399,6 +432,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@ -465,6 +499,7 @@
|
||||
D0C9CBD52302C06E00FAB518 /* ReleaseHockeyappInternal */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEFINES_MODULE = YES;
|
||||
|
Loading…
x
Reference in New Issue
Block a user