mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-29 19:35:08 +00:00
Text selection in story captions
This commit is contained in:
parent
3a0d3b66f4
commit
1272e80e29
@ -79,6 +79,11 @@ swift_library(
|
|||||||
"//submodules/Utils/RangeSet",
|
"//submodules/Utils/RangeSet",
|
||||||
"//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen",
|
"//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen",
|
||||||
"//submodules/TelegramUI/Components/LottieComponent",
|
"//submodules/TelegramUI/Components/LottieComponent",
|
||||||
|
"//submodules/TextSelectionNode",
|
||||||
|
"//submodules/Pasteboard",
|
||||||
|
"//submodules/Speak",
|
||||||
|
"//submodules/TranslateUI",
|
||||||
|
"//submodules/TelegramNotices",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import TextFormat
|
|||||||
import InvisibleInkDustNode
|
import InvisibleInkDustNode
|
||||||
import UrlEscaping
|
import UrlEscaping
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
import TextSelectionNode
|
||||||
|
|
||||||
final class StoryContentCaptionComponent: Component {
|
final class StoryContentCaptionComponent: Component {
|
||||||
enum Action {
|
enum Action {
|
||||||
@ -23,6 +24,7 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
|
|
||||||
final class ExternalState {
|
final class ExternalState {
|
||||||
fileprivate(set) var isExpanded: Bool = false
|
fileprivate(set) var isExpanded: Bool = false
|
||||||
|
fileprivate(set) var isSelectingText: Bool = false
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
}
|
}
|
||||||
@ -51,30 +53,39 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
let externalState: ExternalState
|
let externalState: ExternalState
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let strings: PresentationStrings
|
let strings: PresentationStrings
|
||||||
|
let theme: PresentationTheme
|
||||||
let text: String
|
let text: String
|
||||||
let entities: [MessageTextEntity]
|
let entities: [MessageTextEntity]
|
||||||
let entityFiles: [EngineMedia.Id: TelegramMediaFile]
|
let entityFiles: [EngineMedia.Id: TelegramMediaFile]
|
||||||
let action: (Action) -> Void
|
let action: (Action) -> Void
|
||||||
let longTapAction: (Action) -> Void
|
let longTapAction: (Action) -> Void
|
||||||
|
let textSelectionAction: (NSAttributedString, TextSelectionAction) -> Void
|
||||||
|
let controller: () -> ViewController?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
externalState: ExternalState,
|
externalState: ExternalState,
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
strings: PresentationStrings,
|
strings: PresentationStrings,
|
||||||
|
theme: PresentationTheme,
|
||||||
text: String,
|
text: String,
|
||||||
entities: [MessageTextEntity],
|
entities: [MessageTextEntity],
|
||||||
entityFiles: [EngineMedia.Id: TelegramMediaFile],
|
entityFiles: [EngineMedia.Id: TelegramMediaFile],
|
||||||
action: @escaping (Action) -> Void,
|
action: @escaping (Action) -> Void,
|
||||||
longTapAction: @escaping (Action) -> Void
|
longTapAction: @escaping (Action) -> Void,
|
||||||
|
textSelectionAction: @escaping (NSAttributedString, TextSelectionAction) -> Void,
|
||||||
|
controller: @escaping () -> ViewController?
|
||||||
) {
|
) {
|
||||||
self.externalState = externalState
|
self.externalState = externalState
|
||||||
self.context = context
|
self.context = context
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
|
self.theme = theme
|
||||||
self.text = text
|
self.text = text
|
||||||
self.entities = entities
|
self.entities = entities
|
||||||
self.entityFiles = entityFiles
|
self.entityFiles = entityFiles
|
||||||
self.action = action
|
self.action = action
|
||||||
self.longTapAction = longTapAction
|
self.longTapAction = longTapAction
|
||||||
|
self.textSelectionAction = textSelectionAction
|
||||||
|
self.controller = controller
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: StoryContentCaptionComponent, rhs: StoryContentCaptionComponent) -> Bool {
|
static func ==(lhs: StoryContentCaptionComponent, rhs: StoryContentCaptionComponent) -> Bool {
|
||||||
@ -87,6 +98,9 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
if lhs.strings !== rhs.strings {
|
if lhs.strings !== rhs.strings {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.text != rhs.text {
|
if lhs.text != rhs.text {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -136,6 +150,7 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
|
|
||||||
private let collapsedText: ContentItem
|
private let collapsedText: ContentItem
|
||||||
private let expandedText: ContentItem
|
private let expandedText: ContentItem
|
||||||
|
private var textSelectionNode: TextSelectionNode?
|
||||||
|
|
||||||
private let scrollMaskContainer: UIView
|
private let scrollMaskContainer: UIView
|
||||||
private let scrollFullMaskView: UIView
|
private let scrollFullMaskView: UIView
|
||||||
@ -217,9 +232,6 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
self.scrollView.addSubview(self.expandedText)
|
self.scrollView.addSubview(self.expandedText)
|
||||||
|
|
||||||
self.scrollViewContainer.mask = self.scrollMaskContainer
|
self.scrollViewContainer.mask = self.scrollMaskContainer
|
||||||
|
|
||||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
|
||||||
self.addGestureRecognizer(tapRecognizer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -231,10 +243,12 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let textView = self.collapsedText.textNode?.textNode.view {
|
let contentItem = self.isExpanded ? self.expandedText : self.collapsedText
|
||||||
|
|
||||||
|
if let textView = contentItem.textNode?.textNode.view {
|
||||||
let textLocalPoint = self.convert(point, to: textView)
|
let textLocalPoint = self.convert(point, to: textView)
|
||||||
if textLocalPoint.y >= -7.0 {
|
if textLocalPoint.y >= -7.0 {
|
||||||
return textView
|
return self.textSelectionNode?.view ?? textView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,6 +265,15 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if component.externalState.isSelectingText {
|
||||||
|
self.cancelTextSelection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
if !self.ignoreScrolling {
|
if !self.ignoreScrolling {
|
||||||
self.updateScrolling(transition: .immediate)
|
self.updateScrolling(transition: .immediate)
|
||||||
@ -292,6 +315,10 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
self.updateScrolling(transition: transition.withUserData(InternalTransitionHint(bounceScrolling: true)))
|
self.updateScrolling(transition: transition.withUserData(InternalTransitionHint(bounceScrolling: true)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cancelTextSelection() {
|
||||||
|
self.textSelectionNode?.cancelSelection()
|
||||||
|
}
|
||||||
|
|
||||||
private func updateScrolling(transition: Transition) {
|
private func updateScrolling(transition: Transition) {
|
||||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||||
return
|
return
|
||||||
@ -340,6 +367,12 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||||
|
if let textSelectionNode = self.textSelectionNode {
|
||||||
|
if textSelectionNode.didRecognizeTap {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let contentItem = self.isExpanded ? self.expandedText : self.collapsedText
|
let contentItem = self.isExpanded ? self.expandedText : self.collapsedText
|
||||||
let otherContentItem = !self.isExpanded ? self.expandedText : self.collapsedText
|
let otherContentItem = !self.isExpanded ? self.expandedText : self.collapsedText
|
||||||
|
|
||||||
@ -386,7 +419,9 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if case .tap = gesture {
|
if case .tap = gesture {
|
||||||
if self.isExpanded {
|
if component.externalState.isSelectingText {
|
||||||
|
self.cancelTextSelection()
|
||||||
|
} else if self.isExpanded {
|
||||||
self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
} else {
|
} else {
|
||||||
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
@ -395,7 +430,9 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if case .tap = gesture {
|
if case .tap = gesture {
|
||||||
if self.isExpanded {
|
if component.externalState.isSelectingText {
|
||||||
|
self.cancelTextSelection()
|
||||||
|
} else if self.isExpanded {
|
||||||
self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
} else {
|
} else {
|
||||||
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
@ -545,18 +582,6 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
self.collapsedText.textNode = collapsedTextNode
|
self.collapsedText.textNode = collapsedTextNode
|
||||||
if collapsedTextNode.textNode.view.superview == nil {
|
if collapsedTextNode.textNode.view.superview == nil {
|
||||||
self.collapsedText.addSubview(collapsedTextNode.textNode.view)
|
self.collapsedText.addSubview(collapsedTextNode.textNode.view)
|
||||||
|
|
||||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
|
||||||
recognizer.tapActionAtPoint = { point in
|
|
||||||
return .waitForSingleTap
|
|
||||||
}
|
|
||||||
recognizer.highlight = { [weak self] point in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.updateTouchesAtPoint(point)
|
|
||||||
}
|
|
||||||
collapsedTextNode.textNode.view.addGestureRecognizer(recognizer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
collapsedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
|
collapsedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
|
||||||
@ -623,18 +648,6 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
self.expandedText.textNode = expandedTextNode
|
self.expandedText.textNode = expandedTextNode
|
||||||
if expandedTextNode.textNode.view.superview == nil {
|
if expandedTextNode.textNode.view.superview == nil {
|
||||||
self.expandedText.addSubview(expandedTextNode.textNode.view)
|
self.expandedText.addSubview(expandedTextNode.textNode.view)
|
||||||
|
|
||||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
|
||||||
recognizer.tapActionAtPoint = { point in
|
|
||||||
return .waitForSingleTap
|
|
||||||
}
|
|
||||||
recognizer.highlight = { [weak self] point in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.updateTouchesAtPoint(point)
|
|
||||||
}
|
|
||||||
expandedTextNode.textNode.view.addGestureRecognizer(recognizer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expandedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
|
expandedTextNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0))
|
||||||
@ -687,6 +700,114 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.textSelectionNode == nil, let controller = component.controller(), let textNode = self.expandedText.textNode?.textNode {
|
||||||
|
let selectionColor = UIColor(white: 1.0, alpha: 0.5)
|
||||||
|
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: component.theme.list.itemAccentColor), strings: component.strings, textNode: textNode, updateIsActive: { [weak self] value in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if component.externalState.isSelectingText != value {
|
||||||
|
component.externalState.isSelectingText = value
|
||||||
|
|
||||||
|
if !self.ignoreExternalState {
|
||||||
|
self.state?.updated(transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, present: { [weak self] c, a in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.controller()?.presentInGlobalOverlay(c, with: a)
|
||||||
|
}, rootNode: controller.displayNode, performAction: { [weak self] text, action in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.textSelectionAction(text, action)
|
||||||
|
})
|
||||||
|
/*textSelectionNode.updateRange = { [weak self] selectionRange in
|
||||||
|
if let strongSelf = self, let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange {
|
||||||
|
for (spoilerRange, _) in textLayout.spoilers {
|
||||||
|
if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 {
|
||||||
|
dustNode.update(revealed: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
textSelectionNode.enableLookup = true
|
||||||
|
self.textSelectionNode = textSelectionNode
|
||||||
|
self.scrollView.addSubview(textSelectionNode.view)
|
||||||
|
self.scrollView.insertSubview(textSelectionNode.highlightAreaNode.view, at: 0)
|
||||||
|
|
||||||
|
textSelectionNode.canBeginSelection = { [weak self] location in
|
||||||
|
guard let self else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentItem = self.expandedText
|
||||||
|
guard let textNode = contentItem.textNode else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleFrame = textNode.textNode.view.bounds
|
||||||
|
|
||||||
|
if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
|
||||||
|
let action: Action?
|
||||||
|
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(contentItem.dustNode?.isRevealed ?? true) {
|
||||||
|
return false
|
||||||
|
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||||
|
var concealed = true
|
||||||
|
if let (attributeText, fullText) = textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||||
|
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
||||||
|
}
|
||||||
|
action = .url(url: url, concealed: concealed)
|
||||||
|
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
||||||
|
action = .peerMention(peerId: peerMention.peerId, mention: peerMention.mention)
|
||||||
|
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
||||||
|
action = .textMention(peerName)
|
||||||
|
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
||||||
|
action = .hashtag(hashtag.peerName, hashtag.hashtag)
|
||||||
|
} else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String {
|
||||||
|
action = .bankCard(bankCard)
|
||||||
|
} else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file {
|
||||||
|
action = .customEmoji(file)
|
||||||
|
} else {
|
||||||
|
action = nil
|
||||||
|
}
|
||||||
|
if action != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
//let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||||
|
//textSelectionNode.view.addGestureRecognizer(tapRecognizer)
|
||||||
|
|
||||||
|
let _ = textSelectionNode.view
|
||||||
|
|
||||||
|
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
||||||
|
/*if let selectionRecognizer = textSelectionNode.recognizer {
|
||||||
|
recognizer.require(toFail: selectionRecognizer)
|
||||||
|
}*/
|
||||||
|
recognizer.tapActionAtPoint = { point in
|
||||||
|
return .waitForSingleTap
|
||||||
|
}
|
||||||
|
recognizer.highlight = { [weak self] point in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.updateTouchesAtPoint(point)
|
||||||
|
}
|
||||||
|
textSelectionNode.view.addGestureRecognizer(recognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let textSelectionNode = self.textSelectionNode, let textNode = self.expandedText.textNode?.textNode {
|
||||||
|
textSelectionNode.frame = textNode.frame.offsetBy(dx: self.expandedText.frame.minX, dy: self.expandedText.frame.minY)
|
||||||
|
textSelectionNode.highlightAreaNode.frame = textSelectionNode.frame
|
||||||
|
}
|
||||||
|
|
||||||
self.itemLayout = ItemLayout(
|
self.itemLayout = ItemLayout(
|
||||||
containerSize: availableSize,
|
containerSize: availableSize,
|
||||||
visibleTextHeight: visibleTextHeight,
|
visibleTextHeight: visibleTextHeight,
|
||||||
|
|||||||
@ -36,6 +36,8 @@ import StickerPackPreviewUI
|
|||||||
import TextNodeWithEntities
|
import TextNodeWithEntities
|
||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
import LottieComponent
|
import LottieComponent
|
||||||
|
import Pasteboard
|
||||||
|
import Speak
|
||||||
|
|
||||||
public final class StoryAvailableReactions: Equatable {
|
public final class StoryAvailableReactions: Equatable {
|
||||||
let reactionItems: [ReactionItem]
|
let reactionItems: [ReactionItem]
|
||||||
@ -765,9 +767,13 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let captionItem = self.captionItem, captionItem.externalState.isExpanded {
|
} else if let captionItem = self.captionItem, (captionItem.externalState.isExpanded || captionItem.externalState.isSelectingText) {
|
||||||
if let captionItemView = captionItem.view.view as? StoryContentCaptionComponent.View {
|
if let captionItemView = captionItem.view.view as? StoryContentCaptionComponent.View {
|
||||||
captionItemView.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
if captionItem.externalState.isSelectingText {
|
||||||
|
captionItemView.cancelTextSelection()
|
||||||
|
} else {
|
||||||
|
captionItemView.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let point = recognizer.location(in: self)
|
let point = recognizer.location(in: self)
|
||||||
@ -967,13 +973,16 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
if self.sendMessageContext.statusController != nil {
|
if self.sendMessageContext.statusController != nil {
|
||||||
return .pause
|
return .pause
|
||||||
}
|
}
|
||||||
|
if self.sendMessageContext.lookupController != nil {
|
||||||
|
return .pause
|
||||||
|
}
|
||||||
if let navigationController = component.controller()?.navigationController as? NavigationController {
|
if let navigationController = component.controller()?.navigationController as? NavigationController {
|
||||||
let topViewController = navigationController.topViewController
|
let topViewController = navigationController.topViewController
|
||||||
if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) && !(topViewController is ShareWithPeersScreen) && !(topViewController is AttachmentController) {
|
if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) && !(topViewController is ShareWithPeersScreen) && !(topViewController is AttachmentController) {
|
||||||
return .pause
|
return .pause
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let captionItem = self.captionItem, captionItem.externalState.isExpanded {
|
if let captionItem = self.captionItem, captionItem.externalState.isExpanded || captionItem.externalState.isSelectingText {
|
||||||
return .blurred
|
return .blurred
|
||||||
}
|
}
|
||||||
return .play
|
return .play
|
||||||
@ -2369,36 +2378,29 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
|
|
||||||
let moreButtonSize = self.moreButton.update(
|
let moreButtonSize = self.moreButton.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(MessageInputActionButtonComponent(
|
component: AnyComponent(PlainButtonComponent(
|
||||||
mode: .more,
|
content: AnyComponent(LottieComponent(
|
||||||
action: { _, _, _ in
|
content: LottieComponent.AppBundleContent(
|
||||||
},
|
name: "anim_story_more"
|
||||||
longPressAction: nil,
|
),
|
||||||
switchMediaInputMode: {
|
color: .white,
|
||||||
},
|
startingPosition: .end,
|
||||||
updateMediaCancelFraction: { _ in
|
size: CGSize(width: 30.0, height: 30.0)
|
||||||
},
|
)),
|
||||||
lockMediaRecording: {
|
effectAlignment: .center,
|
||||||
},
|
minSize: CGSize(width: 33.0, height: 64.0),
|
||||||
stopAndPreviewMediaRecording: {
|
action: { [weak self] in
|
||||||
},
|
|
||||||
moreAction: { [weak self] view, gesture in
|
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.performMoreAction(sourceView: view, gesture: gesture)
|
guard let moreButtonView = self.moreButton.view else {
|
||||||
},
|
|
||||||
context: component.context,
|
|
||||||
theme: component.theme,
|
|
||||||
strings: component.strings,
|
|
||||||
presentController: { [weak self] c in
|
|
||||||
guard let self, let component = self.component else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
component.presentController(c, nil)
|
if let animationView = (moreButtonView as? PlainButtonComponent.View)?.contentView as? LottieComponent.View {
|
||||||
},
|
animationView.playOnce()
|
||||||
audioRecorder: nil,
|
}
|
||||||
videoRecordingStatus: nil
|
self.performMoreAction(sourceView: moreButtonView, gesture: nil)
|
||||||
|
}
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: 33.0, height: 64.0)
|
containerSize: CGSize(width: 33.0, height: 64.0)
|
||||||
@ -2410,7 +2412,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
moreButtonView.isUserInteractionEnabled = !component.slice.item.storyItem.isPending
|
moreButtonView.isUserInteractionEnabled = !component.slice.item.storyItem.isPending
|
||||||
transition.setFrame(view: moreButtonView, frame: CGRect(origin: CGPoint(x: headerRightOffset - moreButtonSize.width, y: 2.0), size: moreButtonSize))
|
transition.setFrame(view: moreButtonView, frame: CGRect(origin: CGPoint(x: headerRightOffset - moreButtonSize.width, y: 2.0), size: moreButtonSize))
|
||||||
transition.setAlpha(view: moreButtonView, alpha: component.slice.item.storyItem.isPending ? 0.5 : 1.0)
|
transition.setAlpha(view: moreButtonView, alpha: component.slice.item.storyItem.isPending ? 0.5 : 1.0)
|
||||||
headerRightOffset -= moreButtonSize.width + 15.0
|
headerRightOffset -= moreButtonSize.width + 12.0
|
||||||
}
|
}
|
||||||
|
|
||||||
var isSilentVideo = false
|
var isSilentVideo = false
|
||||||
@ -2762,6 +2764,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
externalState: captionItem.externalState,
|
externalState: captionItem.externalState,
|
||||||
context: component.context,
|
context: component.context,
|
||||||
strings: component.strings,
|
strings: component.strings,
|
||||||
|
theme: component.theme,
|
||||||
text: component.slice.item.storyItem.text,
|
text: component.slice.item.storyItem.text,
|
||||||
entities: component.slice.item.storyItem.entities,
|
entities: component.slice.item.storyItem.entities,
|
||||||
entityFiles: component.slice.item.entityFiles,
|
entityFiles: component.slice.item.entityFiles,
|
||||||
@ -2811,6 +2814,39 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.sendMessageContext.openResolved(view: self, result: resolved, forceExternal: false, concealed: concealed)
|
self.sendMessageContext.openResolved(view: self, result: resolved, forceExternal: false, concealed: concealed)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
textSelectionAction: { [weak self] text, action in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch action {
|
||||||
|
case .copy:
|
||||||
|
storeAttributedTextInPasteboard(text)
|
||||||
|
case .share:
|
||||||
|
self.sendMessageContext.performShareTextAction(view: self, text: text.string)
|
||||||
|
case .lookup:
|
||||||
|
self.sendMessageContext.performLookupTextAction(view: self, text: text.string)
|
||||||
|
case .speak:
|
||||||
|
if let speechHolder = speakText(context: component.context, text: text.string) {
|
||||||
|
speechHolder.completion = { [weak self, weak speechHolder] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.sendMessageContext.currentSpeechHolder === speechHolder {
|
||||||
|
self.sendMessageContext.currentSpeechHolder = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.sendMessageContext.currentSpeechHolder = speechHolder
|
||||||
|
}
|
||||||
|
case .translate:
|
||||||
|
self.sendMessageContext.performTranslateTextAction(view: self, text: text.string)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controller: { [weak self] in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return component.controller()
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
|
|||||||
@ -43,6 +43,12 @@ import WebPBinding
|
|||||||
import ContextUI
|
import ContextUI
|
||||||
import ChatScheduleTimeController
|
import ChatScheduleTimeController
|
||||||
import StoryStealthModeSheetScreen
|
import StoryStealthModeSheetScreen
|
||||||
|
import Speak
|
||||||
|
import TranslateUI
|
||||||
|
import TelegramNotices
|
||||||
|
import ObjectiveC
|
||||||
|
|
||||||
|
private var ObjCKey_DeinitWatcher: Int?
|
||||||
|
|
||||||
final class StoryItemSetContainerSendMessage {
|
final class StoryItemSetContainerSendMessage {
|
||||||
enum InputMode {
|
enum InputMode {
|
||||||
@ -59,6 +65,7 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
weak var tooltipScreen: ViewController?
|
weak var tooltipScreen: ViewController?
|
||||||
weak var actionSheet: ViewController?
|
weak var actionSheet: ViewController?
|
||||||
weak var statusController: ViewController?
|
weak var statusController: ViewController?
|
||||||
|
weak var lookupController: UIViewController?
|
||||||
var isViewingAttachedStickers = false
|
var isViewingAttachedStickers = false
|
||||||
|
|
||||||
var currentTooltipUpdateTimer: Foundation.Timer?
|
var currentTooltipUpdateTimer: Foundation.Timer?
|
||||||
@ -86,6 +93,8 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
let navigationActionDisposable = MetaDisposable()
|
let navigationActionDisposable = MetaDisposable()
|
||||||
let resolvePeerByNameDisposable = MetaDisposable()
|
let resolvePeerByNameDisposable = MetaDisposable()
|
||||||
|
|
||||||
|
var currentSpeechHolder: SpeechSynthesizerHolder?
|
||||||
|
|
||||||
private(set) var isMediaRecordingLocked: Bool = false
|
private(set) var isMediaRecordingLocked: Bool = false
|
||||||
var wasRecordingDismissed: Bool = false
|
var wasRecordingDismissed: Bool = false
|
||||||
|
|
||||||
@ -1016,6 +1025,129 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func performShareTextAction(view: StoryItemSetContainerComponent.View, text: String) {
|
||||||
|
guard let component = view.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let controller = component.controller() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let theme = component.theme
|
||||||
|
let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) })
|
||||||
|
|
||||||
|
let shareController = ShareController(context: component.context, subject: .text(text), externalShare: true, immediateExternalShare: false, updatedPresentationData: updatedPresentationData)
|
||||||
|
|
||||||
|
self.shareController = shareController
|
||||||
|
view.updateIsProgressPaused()
|
||||||
|
|
||||||
|
shareController.dismissed = { [weak self, weak view] _ in
|
||||||
|
guard let self, let view else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.shareController = nil
|
||||||
|
view.updateIsProgressPaused()
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.present(shareController, in: .window(.root))
|
||||||
|
}
|
||||||
|
|
||||||
|
func performTranslateTextAction(view: StoryItemSetContainerComponent.View, text: String) {
|
||||||
|
guard let component = view.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = (component.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self, weak view] sharedData in
|
||||||
|
guard let self, let view else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let peer = component.slice.peer
|
||||||
|
|
||||||
|
let _ = self
|
||||||
|
|
||||||
|
let translationSettings: TranslationSettings
|
||||||
|
if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) {
|
||||||
|
translationSettings = current
|
||||||
|
} else {
|
||||||
|
translationSettings = TranslationSettings.defaultSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
var showTranslateIfTopical = false
|
||||||
|
if case let .channel(channel) = peer, !(channel.addressName ?? "").isEmpty {
|
||||||
|
showTranslateIfTopical = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, language) = canTranslateText(context: component.context, text: text, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: showTranslateIfTopical, ignoredLanguages: translationSettings.ignoredLanguages)
|
||||||
|
|
||||||
|
let _ = ApplicationSpecificNotice.incrementTranslationSuggestion(accountManager: component.context.sharedContext.accountManager, timestamp: Int32(Date().timeIntervalSince1970)).start()
|
||||||
|
|
||||||
|
let translateController = TranslateScreen(context: component.context, text: text, canCopy: true, fromLanguage: language)
|
||||||
|
translateController.pushController = { [weak view] c in
|
||||||
|
guard let view, let component = view.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.controller()?.push(c)
|
||||||
|
}
|
||||||
|
translateController.presentController = { [weak view] c in
|
||||||
|
guard let view, let component = view.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.controller()?.present(c, in: .window(.root))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.actionSheet = translateController
|
||||||
|
view.updateIsProgressPaused()
|
||||||
|
|
||||||
|
translateController.wasDismissed = { [weak self, weak view] in
|
||||||
|
guard let self, let view else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.actionSheet = nil
|
||||||
|
view.updateIsProgressPaused()
|
||||||
|
}
|
||||||
|
|
||||||
|
component.controller()?.present(translateController, in: .window(.root))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func performLookupTextAction(view: StoryItemSetContainerComponent.View, text: String) {
|
||||||
|
guard let component = view.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let controller = UIReferenceLibraryViewController(term: text)
|
||||||
|
if let window = component.controller()?.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)
|
||||||
|
|
||||||
|
final class DeinitWatcher: NSObject {
|
||||||
|
let f: () -> Void
|
||||||
|
|
||||||
|
init(_ f: @escaping () -> Void) {
|
||||||
|
self.f = f
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.lookupController = controller
|
||||||
|
view.updateIsProgressPaused()
|
||||||
|
|
||||||
|
objc_setAssociatedObject(controller, &ObjCKey_DeinitWatcher, DeinitWatcher { [weak self, weak view] in
|
||||||
|
guard let self, let view else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.lookupController = nil
|
||||||
|
view.updateIsProgressPaused()
|
||||||
|
}, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func performCopyLinkAction(view: StoryItemSetContainerComponent.View) {
|
func performCopyLinkAction(view: StoryItemSetContainerComponent.View) {
|
||||||
guard let component = view.component else {
|
guard let component = view.component else {
|
||||||
return
|
return
|
||||||
|
|||||||
BIN
submodules/TelegramUI/Resources/Animations/anim_story_more.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/anim_story_more.tgs
Normal file
Binary file not shown.
@ -74,18 +74,21 @@ private enum Knob {
|
|||||||
case right
|
case right
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
|
public final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
|
||||||
private var longTapTimer: Timer?
|
private var longTapTimer: Timer?
|
||||||
private var movingKnob: (Knob, CGPoint, CGPoint)?
|
private var movingKnob: (Knob, CGPoint, CGPoint)?
|
||||||
private var currentLocation: CGPoint?
|
private var currentLocation: CGPoint?
|
||||||
|
|
||||||
var beginSelection: ((CGPoint) -> Void)?
|
public var canBeginSelection: ((CGPoint) -> Bool)?
|
||||||
var knobAtPoint: ((CGPoint) -> (Knob, CGPoint)?)?
|
public var beginSelection: ((CGPoint) -> Void)?
|
||||||
var moveKnob: ((Knob, CGPoint) -> Void)?
|
fileprivate var knobAtPoint: ((CGPoint) -> (Knob, CGPoint)?)?
|
||||||
var finishedMovingKnob: (() -> Void)?
|
fileprivate var moveKnob: ((Knob, CGPoint) -> Void)?
|
||||||
var clearSelection: (() -> Void)?
|
public var finishedMovingKnob: (() -> Void)?
|
||||||
|
public var clearSelection: (() -> Void)?
|
||||||
|
public private(set) var didRecognizeTap: Bool = false
|
||||||
|
fileprivate var isSelecting: Bool = false
|
||||||
|
|
||||||
override init(target: Any?, action: Selector?) {
|
override public init(target: Any?, action: Selector?) {
|
||||||
super.init(target: nil, action: nil)
|
super.init(target: nil, action: nil)
|
||||||
|
|
||||||
self.delegate = self
|
self.delegate = self
|
||||||
@ -101,7 +104,7 @@ private final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestu
|
|||||||
self.currentLocation = nil
|
self.currentLocation = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
super.touchesBegan(touches, with: event)
|
super.touchesBegan(touches, with: event)
|
||||||
|
|
||||||
let currentLocation = touches.first?.location(in: self.view)
|
let currentLocation = touches.first?.location(in: self.view)
|
||||||
@ -112,28 +115,32 @@ private final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestu
|
|||||||
self.movingKnob = (knob, knobPosition, currentLocation)
|
self.movingKnob = (knob, knobPosition, currentLocation)
|
||||||
cancelScrollViewGestures(view: self.view?.superview)
|
cancelScrollViewGestures(view: self.view?.superview)
|
||||||
self.state = .began
|
self.state = .began
|
||||||
} else if self.longTapTimer == nil {
|
} else if self.canBeginSelection?(currentLocation) ?? true {
|
||||||
final class TimerTarget: NSObject {
|
if self.longTapTimer == nil {
|
||||||
let f: () -> Void
|
final class TimerTarget: NSObject {
|
||||||
|
let f: () -> Void
|
||||||
init(_ f: @escaping () -> Void) {
|
|
||||||
self.f = f
|
init(_ f: @escaping () -> Void) {
|
||||||
}
|
self.f = f
|
||||||
|
}
|
||||||
@objc func event() {
|
|
||||||
self.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)
|
||||||
}
|
}
|
||||||
let longTapTimer = Timer(timeInterval: 0.3, target: TimerTarget({ [weak self] in
|
} else {
|
||||||
self?.longTapEvent()
|
self.state = .failed
|
||||||
}), 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) {
|
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
super.touchesMoved(touches, with: event)
|
super.touchesMoved(touches, with: event)
|
||||||
|
|
||||||
let currentLocation = touches.first?.location(in: self.view)
|
let currentLocation = touches.first?.location(in: self.view)
|
||||||
@ -144,12 +151,20 @@ private final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
super.touchesEnded(touches, with: event)
|
super.touchesEnded(touches, with: event)
|
||||||
|
|
||||||
if let longTapTimer = self.longTapTimer {
|
if let longTapTimer = self.longTapTimer {
|
||||||
self.longTapTimer = nil
|
self.longTapTimer = nil
|
||||||
longTapTimer.invalidate()
|
longTapTimer.invalidate()
|
||||||
|
|
||||||
|
if self.isSelecting {
|
||||||
|
self.didRecognizeTap = true
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.didRecognizeTap = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.clearSelection?()
|
self.clearSelection?()
|
||||||
} else {
|
} else {
|
||||||
if let _ = self.currentLocation, let _ = self.movingKnob {
|
if let _ = self.currentLocation, let _ = self.movingKnob {
|
||||||
@ -159,7 +174,7 @@ private final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestu
|
|||||||
self.state = .ended
|
self.state = .ended
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
super.touchesCancelled(touches, with: event)
|
super.touchesCancelled(touches, with: event)
|
||||||
|
|
||||||
self.state = .cancelled
|
self.state = .cancelled
|
||||||
@ -172,12 +187,11 @@ private final class TextSelectionGestureRecognizer: UIGestureRecognizer, UIGestu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 9.0, *)
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool {
|
||||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,6 +217,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
private let strings: PresentationStrings
|
private let strings: PresentationStrings
|
||||||
private let textNode: TextNode
|
private let textNode: TextNode
|
||||||
private let updateIsActive: (Bool) -> Void
|
private let updateIsActive: (Bool) -> Void
|
||||||
|
public var canBeginSelection: (CGPoint) -> Bool = { _ in true }
|
||||||
public var updateRange: ((NSRange?) -> Void)?
|
public var updateRange: ((NSRange?) -> Void)?
|
||||||
private let present: (ViewController, Any?) -> Void
|
private let present: (ViewController, Any?) -> Void
|
||||||
private weak var rootNode: ASDisplayNode?
|
private weak var rootNode: ASDisplayNode?
|
||||||
@ -216,9 +231,15 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
|
|
||||||
public let highlightAreaNode: ASDisplayNode
|
public let highlightAreaNode: ASDisplayNode
|
||||||
|
|
||||||
private var recognizer: TextSelectionGestureRecognizer?
|
public private(set) var recognizer: TextSelectionGestureRecognizer?
|
||||||
private var displayLinkAnimator: DisplayLinkAnimator?
|
private var displayLinkAnimator: DisplayLinkAnimator?
|
||||||
|
|
||||||
|
public var enableLookup: Bool = true
|
||||||
|
|
||||||
|
public var didRecognizeTap: Bool {
|
||||||
|
return self.recognizer?.didRecognizeTap ?? false
|
||||||
|
}
|
||||||
|
|
||||||
public init(theme: TextSelectionTheme, strings: PresentationStrings, textNode: TextNode, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: ASDisplayNode, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) {
|
public init(theme: TextSelectionTheme, strings: PresentationStrings, textNode: TextNode, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: ASDisplayNode, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
@ -332,12 +353,19 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
strongSelf.updateSelection(range: resultRange, animateIn: true)
|
strongSelf.updateSelection(range: resultRange, animateIn: true)
|
||||||
strongSelf.displayMenu()
|
strongSelf.displayMenu()
|
||||||
|
strongSelf.recognizer?.isSelecting = true
|
||||||
strongSelf.updateIsActive(true)
|
strongSelf.updateIsActive(true)
|
||||||
}
|
}
|
||||||
recognizer.clearSelection = { [weak self] in
|
recognizer.clearSelection = { [weak self] in
|
||||||
self?.dismissSelection()
|
self?.dismissSelection()
|
||||||
self?.updateIsActive(false)
|
self?.updateIsActive(false)
|
||||||
}
|
}
|
||||||
|
recognizer.canBeginSelection = { [weak self] point in
|
||||||
|
guard let self else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return self.canBeginSelection(point)
|
||||||
|
}
|
||||||
self.recognizer = recognizer
|
self.recognizer = recognizer
|
||||||
self.view.addGestureRecognizer(recognizer)
|
self.view.addGestureRecognizer(recognizer)
|
||||||
}
|
}
|
||||||
@ -487,9 +515,15 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
|
|
||||||
private func dismissSelection() {
|
private func dismissSelection() {
|
||||||
self.currentRange = nil
|
self.currentRange = nil
|
||||||
|
self.recognizer?.isSelecting = false
|
||||||
self.updateSelection(range: nil, animateIn: false)
|
self.updateSelection(range: nil, animateIn: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func cancelSelection() {
|
||||||
|
self.dismissSelection()
|
||||||
|
self.updateIsActive(false)
|
||||||
|
}
|
||||||
|
|
||||||
private func displayMenu() {
|
private func displayMenu() {
|
||||||
guard let currentRects = self.currentRects, !currentRects.isEmpty, let currentRange = self.currentRange, let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else {
|
guard let currentRects = self.currentRects, !currentRects.isEmpty, let currentRange = self.currentRange, let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else {
|
||||||
return
|
return
|
||||||
@ -529,16 +563,18 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
var actions: [ContextMenuAction] = []
|
var actions: [ContextMenuAction] = []
|
||||||
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
|
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
|
||||||
self?.performAction(string, .copy)
|
self?.performAction(string, .copy)
|
||||||
self?.dismissSelection()
|
self?.cancelSelection()
|
||||||
}))
|
|
||||||
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuLookUp, accessibilityLabel: self.strings.Conversation_ContextMenuLookUp), action: { [weak self] in
|
|
||||||
self?.performAction(string, .lookup)
|
|
||||||
self?.dismissSelection()
|
|
||||||
}))
|
}))
|
||||||
|
if self.enableLookup {
|
||||||
|
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuLookUp, accessibilityLabel: self.strings.Conversation_ContextMenuLookUp), action: { [weak self] in
|
||||||
|
self?.performAction(string, .lookup)
|
||||||
|
self?.cancelSelection()
|
||||||
|
}))
|
||||||
|
}
|
||||||
if #available(iOS 15.0, *) {
|
if #available(iOS 15.0, *) {
|
||||||
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in
|
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in
|
||||||
self?.performAction(string, .translate)
|
self?.performAction(string, .translate)
|
||||||
self?.dismissSelection()
|
self?.cancelSelection()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
// if isSpeakSelectionEnabled() {
|
// if isSpeakSelectionEnabled() {
|
||||||
@ -549,7 +585,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
// }
|
// }
|
||||||
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
|
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
|
||||||
self?.performAction(string, .share)
|
self?.performAction(string, .share)
|
||||||
self?.dismissSelection()
|
self?.cancelSelection()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
self.present(ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
self.present(ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||||||
|
|||||||
@ -990,6 +990,8 @@ public class TranslateScreen: ViewController {
|
|||||||
public var pushController: (ViewController) -> Void = { _ in }
|
public var pushController: (ViewController) -> Void = { _ in }
|
||||||
public var presentController: (ViewController) -> Void = { _ in }
|
public var presentController: (ViewController) -> Void = { _ in }
|
||||||
|
|
||||||
|
public var wasDismissed: (() -> Void)?
|
||||||
|
|
||||||
public convenience init(context: AccountContext, text: String, canCopy: Bool, fromLanguage: String?, toLanguage: String? = nil, isExpanded: Bool = false) {
|
public convenience init(context: AccountContext, text: String, canCopy: Bool, fromLanguage: String?, toLanguage: String? = nil, isExpanded: Bool = false) {
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
@ -1086,13 +1088,16 @@ public class TranslateScreen: ViewController {
|
|||||||
|
|
||||||
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||||
self.view.endEditing(true)
|
self.view.endEditing(true)
|
||||||
|
let wasDismissed = self.wasDismissed
|
||||||
if flag {
|
if flag {
|
||||||
self.node.animateOut(completion: {
|
self.node.animateOut(completion: {
|
||||||
super.dismiss(animated: false, completion: {})
|
super.dismiss(animated: false, completion: {})
|
||||||
|
wasDismissed?()
|
||||||
completion?()
|
completion?()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
super.dismiss(animated: false, completion: {})
|
super.dismiss(animated: false, completion: {})
|
||||||
|
wasDismissed?()
|
||||||
completion?()
|
completion?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user