mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Preserve text formatting when copying a part of message text
This commit is contained in:
parent
b99f4b6988
commit
5a6963b125
@ -1874,13 +1874,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
switch action {
|
||||
case .copy:
|
||||
UIPasteboard.general.string = text
|
||||
storeAttributedTextInPasteboard(text)
|
||||
case .share:
|
||||
let f = {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let shareController = ShareController(context: strongSelf.context, subject: .text(text), externalShare: true, immediateExternalShare: false)
|
||||
let shareController = ShareController(context: strongSelf.context, subject: .text(text.string), externalShare: true, immediateExternalShare: false)
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
strongSelf.present(shareController, in: .window(.root))
|
||||
}
|
||||
@ -1892,7 +1892,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
f()
|
||||
}
|
||||
case .lookup:
|
||||
let controller = UIReferenceLibraryViewController(term: text)
|
||||
let controller = UIReferenceLibraryViewController(term: text.string)
|
||||
if let window = strongSelf.effectiveNavigationController?.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))
|
||||
|
@ -100,7 +100,7 @@ public final class ChatControllerInteraction {
|
||||
let scheduleCurrentMessage: () -> Void
|
||||
let sendScheduledMessagesNow: ([MessageId]) -> Void
|
||||
let editScheduledMessagesTime: ([MessageId]) -> Void
|
||||
let performTextSelectionAction: (UInt32, String, TextSelectionAction) -> Void
|
||||
let performTextSelectionAction: (UInt32, NSAttributedString, TextSelectionAction) -> Void
|
||||
let updateMessageReaction: (MessageId, String?) -> Void
|
||||
let openMessageReactions: (MessageId) -> Void
|
||||
let displaySwipeToReplyHint: () -> Void
|
||||
@ -126,7 +126,7 @@ public final class ChatControllerInteraction {
|
||||
var searchTextHighightState: (String, [MessageIndex])?
|
||||
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, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> 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?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @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?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, 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, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> 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, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void, displayDiceTooltip: @escaping (TelegramMediaDice) -> Void, animateDiceSuccess: @escaping () -> 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, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> 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?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @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?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, 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, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> 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, NSAttributedString, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void, displayDiceTooltip: @escaping (TelegramMediaDice) -> Void, animateDiceSuccess: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
self.openMessage = openMessage
|
||||
self.openPeer = openPeer
|
||||
self.openPeerMention = openPeerMention
|
||||
|
@ -26,28 +26,31 @@ private func rtfStringWithAppliedEntities(_ text: String, entities: [MessageText
|
||||
|
||||
func chatInputStateStringFromRTF(_ data: Data, type: NSAttributedString.DocumentType) -> NSAttributedString? {
|
||||
if let attributedString = try? NSAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType: type], documentAttributes: nil) {
|
||||
|
||||
let string = NSMutableAttributedString(string: attributedString.string)
|
||||
attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: [], using: { attributes, range, _ in
|
||||
if let value = attributes[.link], let url = (value as? URL)?.absoluteString {
|
||||
string.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: range)
|
||||
}
|
||||
else if let value = attributes[.font], let font = value as? UIFont {
|
||||
let fontName = font.fontName.lowercased()
|
||||
if fontName.contains("bold") {
|
||||
string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range)
|
||||
} else if fontName.contains("italic") {
|
||||
string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range)
|
||||
} else if fontName.contains("menlo") || fontName.contains("courier") || fontName.contains("sfmono") {
|
||||
string.addAttribute(ChatTextInputAttributes.monospace, value: true as NSNumber, range: range)
|
||||
}
|
||||
}
|
||||
})
|
||||
return string
|
||||
return chatInputStateString(attributedString: attributedString)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func chatInputStateString(attributedString: NSAttributedString) -> NSAttributedString? {
|
||||
let string = NSMutableAttributedString(string: attributedString.string)
|
||||
attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: [], using: { attributes, range, _ in
|
||||
if let value = attributes[.link], let url = (value as? URL)?.absoluteString {
|
||||
string.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: range)
|
||||
}
|
||||
else if let value = attributes[.font], let font = value as? UIFont {
|
||||
let fontName = font.fontName.lowercased()
|
||||
if fontName.contains("bold") {
|
||||
string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range)
|
||||
} else if fontName.contains("italic") {
|
||||
string.addAttribute(ChatTextInputAttributes.italic, value: true as NSNumber, range: range)
|
||||
} else if fontName.contains("menlo") || fontName.contains("courier") || fontName.contains("sfmono") {
|
||||
string.addAttribute(ChatTextInputAttributes.monospace, value: true as NSNumber, range: range)
|
||||
}
|
||||
}
|
||||
})
|
||||
return string
|
||||
}
|
||||
|
||||
func storeMessageTextInPasteboard(_ text: String, entities: [MessageTextEntity]?) {
|
||||
var items: [String: Any] = [:]
|
||||
items[kUTTypeUTF8PlainText as String] = text
|
||||
@ -58,6 +61,13 @@ func storeMessageTextInPasteboard(_ text: String, entities: [MessageTextEntity]?
|
||||
UIPasteboard.general.items = [items]
|
||||
}
|
||||
|
||||
func storeAttributedTextInPasteboard(_ text: NSAttributedString) {
|
||||
if let inputText = chatInputStateString(attributedString: text) {
|
||||
let entities = generateChatInputTextEntities(inputText)
|
||||
storeMessageTextInPasteboard(inputText.string, entities: entities)
|
||||
}
|
||||
}
|
||||
|
||||
func storeInputTextInPasteboard(_ text: NSAttributedString) {
|
||||
let entities = generateChatInputTextEntities(text)
|
||||
storeMessageTextInPasteboard(text.string, entities: entities)
|
||||
|
@ -196,7 +196,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
||||
private let updateIsActive: (Bool) -> Void
|
||||
private let present: (ViewController, Any?) -> Void
|
||||
private weak var rootNode: ASDisplayNode?
|
||||
private let performAction: (String, TextSelectionAction) -> Void
|
||||
private let performAction: (NSAttributedString, TextSelectionAction) -> Void
|
||||
private var highlightOverlay: LinkHighlightingNode?
|
||||
private let leftKnob: ASImageNode
|
||||
private let rightKnob: ASImageNode
|
||||
@ -209,7 +209,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
||||
private var recognizer: TextSelectionGetureRecognizer?
|
||||
private var displayLinkAnimator: DisplayLinkAnimator?
|
||||
|
||||
public init(theme: TextSelectionTheme, strings: PresentationStrings, textNode: TextNode, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: ASDisplayNode, performAction: @escaping (String, 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.strings = strings
|
||||
self.textNode = textNode
|
||||
@ -377,7 +377,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
public func pretendExtendSelection(to index: Int) {
|
||||
guard let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString, let endRangeRect = cachedLayout.rangeRects(in: NSRange(location: index, length: 1))?.rects.first else {
|
||||
guard let cachedLayout = self.textNode.cachedLayout, let _ = cachedLayout.attributedString, let endRangeRect = cachedLayout.rangeRects(in: NSRange(location: index, length: 1))?.rects.first else {
|
||||
return
|
||||
}
|
||||
let startPoint = self.rightKnob.frame.center
|
||||
@ -492,19 +492,19 @@ public final class TextSelectionNode: ASDisplayNode {
|
||||
}
|
||||
completeRect = completeRect.insetBy(dx: 0.0, dy: -12.0)
|
||||
|
||||
let text = (attributedString.string as NSString).substring(with: range)
|
||||
let attributedText = attributedString.attributedSubstring(from: range)
|
||||
|
||||
var actions: [ContextMenuAction] = []
|
||||
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
|
||||
self?.performAction(text, .copy)
|
||||
self?.performAction(attributedText, .copy)
|
||||
self?.dismissSelection()
|
||||
}))
|
||||
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuLookUp, accessibilityLabel: self.strings.Conversation_ContextMenuLookUp), action: { [weak self] in
|
||||
self?.performAction(text, .lookup)
|
||||
self?.performAction(attributedText, .lookup)
|
||||
self?.dismissSelection()
|
||||
}))
|
||||
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
|
||||
self?.performAction(text, .share)
|
||||
self?.performAction(attributedText, .share)
|
||||
self?.dismissSelection()
|
||||
}))
|
||||
self.present(ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||||
|
Loading…
x
Reference in New Issue
Block a user