mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
b43fa0ecb5
commit
75ee418716
@ -147,11 +147,12 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
||||
return true
|
||||
}
|
||||
|
||||
private var emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
|
||||
private var emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute, CGFloat)] = []
|
||||
func updateEntities() {
|
||||
self.textView.drawingLayoutManager.ensureLayout(for: self.textView.textContainer)
|
||||
|
||||
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
|
||||
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute, CGFloat)] = []
|
||||
let fontSize = self.displayFontSize * 0.78
|
||||
|
||||
var shouldRepeat = false
|
||||
if let attributedText = self.textView.attributedText {
|
||||
@ -160,7 +161,11 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
||||
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||
if let start = self.textView.position(from: beginning, offset: range.location), let end = self.textView.position(from: start, offset: range.length), let textRange = self.textView.textRange(from: start, to: end) {
|
||||
let rect = self.textView.firstRect(for: textRange)
|
||||
customEmojiRects.append((rect, value))
|
||||
var emojiFontSize = fontSize
|
||||
if let font = attributes[.font] as? UIFont {
|
||||
emojiFontSize = font.pointSize
|
||||
}
|
||||
customEmojiRects.append((rect, value, emojiFontSize))
|
||||
if rect.origin.x.isInfinite {
|
||||
shouldRepeat = true
|
||||
}
|
||||
@ -202,7 +207,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
||||
self.customEmojiContainerView = customEmojiContainerView
|
||||
}
|
||||
|
||||
customEmojiContainerView.update(fontSize: self.displayFontSize * 0.78, textColor: textColor, emojiRects: customEmojiRects)
|
||||
customEmojiContainerView.update(fontSize: fontSize, textColor: textColor, emojiRects: customEmojiRects)
|
||||
} else if let customEmojiContainerView = self.customEmojiContainerView {
|
||||
customEmojiContainerView.removeFromSuperview()
|
||||
self.customEmojiContainerView = nil
|
||||
@ -739,13 +744,12 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
||||
let scale = self.textEntity.scale
|
||||
let rotation = self.textEntity.rotation
|
||||
|
||||
let itemSize: CGFloat = floor(24.0 * self.displayFontSize * 0.78 / 17.0)
|
||||
|
||||
var entities: [DrawingEntity] = []
|
||||
for (emojiRect, emojiAttribute) in self.emojiRects {
|
||||
for (emojiRect, emojiAttribute, fontSize) in self.emojiRects {
|
||||
guard let file = emojiAttribute.file else {
|
||||
continue
|
||||
}
|
||||
let itemSize: CGFloat = floor(24.0 * fontSize * 0.78 / 17.0)
|
||||
let emojiTextPosition = emojiRect.center.offsetBy(dx: -textSize.width / 2.0, dy: -textSize.height / 2.0)
|
||||
|
||||
let entity = DrawingStickerEntity(content: .file(file, .sticker))
|
||||
|
@ -1802,7 +1802,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
item.controllerInteraction.performTextSelectionAction(item.message, true, text, action)
|
||||
})
|
||||
textSelectionNode.enableQuote = true
|
||||
textSelectionNode.enableQuote = false
|
||||
self.textSelectionNode = textSelectionNode
|
||||
self.textClippingNode.addSubnode(textSelectionNode)
|
||||
self.textClippingNode.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode)
|
||||
|
@ -616,8 +616,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if case let .reply(info) = info {
|
||||
if strongSelf.textSelectionNode == nil {
|
||||
strongSelf.updateIsExtractedToContextPreview(true)
|
||||
if let initialQuote = info.quote, item.message.id == initialQuote.messageId, let string = strongSelf.textNode.textNode.cachedLayout?.attributedString {
|
||||
let nsString = string.string as NSString
|
||||
if let initialQuote = info.quote, item.message.id == initialQuote.messageId {
|
||||
let nsString = item.message.text as NSString
|
||||
let subRange = nsString.range(of: initialQuote.text)
|
||||
if subRange.location != NSNotFound {
|
||||
strongSelf.beginTextSelection(range: subRange, displayMenu: true)
|
||||
@ -1119,6 +1119,9 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if !item.controllerInteraction.canSendMessages() && !enableCopy {
|
||||
enableQuote = false
|
||||
}
|
||||
if item.message.id.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
enableQuote = false
|
||||
}
|
||||
|
||||
textSelectionNode.enableQuote = enableQuote
|
||||
textSelectionNode.enableTranslate = enableOtherActions
|
||||
|
@ -136,6 +136,13 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
|
||||
guard let self, let item = self.item, let webPage = self.webPage, case let .Loaded(content) = webPage.content else {
|
||||
return ChatMessageBubbleContentTapAction(content: .none)
|
||||
}
|
||||
|
||||
if let file = content.file {
|
||||
if !file.isVideo, !file.isVideoSticker, !file.isAnimated, !file.isAnimatedSticker, !file.isSticker, !file.isMusic {
|
||||
return ChatMessageBubbleContentTapAction(content: .openMessage)
|
||||
}
|
||||
}
|
||||
|
||||
var isConcealed = true
|
||||
if item.message.text.contains(content.url) {
|
||||
isConcealed = false
|
||||
|
@ -75,7 +75,6 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor
|
||||
|
||||
self.titleNode = CompositeTextNode()
|
||||
self.titleNode.imageTintColor = theme.chat.inputPanel.panelControlAccentColor
|
||||
|
||||
self.textNode = ImmediateTextNodeWithEntities()
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
@ -243,29 +242,30 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
let icon: UIImage?
|
||||
icon = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon")?.withRenderingMode(.alwaysTemplate)
|
||||
|
||||
//TODO:localize
|
||||
if let _ = strongSelf.quote {
|
||||
if let icon {
|
||||
let string = "Reply to Quote by "
|
||||
titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: .white))]
|
||||
titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor))]
|
||||
titleText.append(.icon(icon))
|
||||
titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(15.0), textColor: .white)))
|
||||
titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor)))
|
||||
}
|
||||
} else {
|
||||
if let icon {
|
||||
let string = "Reply to "
|
||||
titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: .white))]
|
||||
titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor))]
|
||||
titleText.append(.icon(icon))
|
||||
titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(15.0), textColor: .white)))
|
||||
titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let _ = strongSelf.quote {
|
||||
//TODO:localize
|
||||
let string = "Reply to Quote by \(authorName)"
|
||||
titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: .white))]
|
||||
titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor))]
|
||||
} else {
|
||||
let string = strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string
|
||||
titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: .white))]
|
||||
titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor))]
|
||||
}
|
||||
|
||||
if strongSelf.messageId.peerId != strongSelf.chatPeerId {
|
||||
@ -276,9 +276,9 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
} else {
|
||||
icon = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextGroupIcon")?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
if let icon {
|
||||
titleText.append(.icon(icon))
|
||||
titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(15.0), textColor: .white)))
|
||||
if let iconImage = generateTintedImage(image: icon, color: strongSelf.theme.chat.inputPanel.panelControlAccentColor) {
|
||||
titleText.append(.icon(iconImage))
|
||||
titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -387,10 +387,25 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme)
|
||||
self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor
|
||||
|
||||
self.titleNode.imageTintColor = theme.chat.inputPanel.panelControlAccentColor
|
||||
self.titleNode.components = self.titleNode.components.map { item in
|
||||
switch item {
|
||||
case let .text(text):
|
||||
let updatedText = NSMutableAttributedString(attributedString: text)
|
||||
updatedText.addAttribute(.foregroundColor, value: theme.chat.inputPanel.panelControlAccentColor, range: NSRange(location: 0, length: updatedText.length))
|
||||
return .text(updatedText)
|
||||
case let .icon(icon):
|
||||
if let iconImage = generateTintedImage(image: icon, color: theme.chat.inputPanel.panelControlAccentColor) {
|
||||
return .icon(iconImage)
|
||||
} else {
|
||||
return .icon(icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let text = self.textNode.attributedText?.string {
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.textIsOptions ? self.theme.chat.inputPanel.secondaryTextColor : self.theme.chat.inputPanel.primaryTextColor)
|
||||
if let text = self.textNode.attributedText {
|
||||
let updatedText = NSMutableAttributedString(attributedString: text)
|
||||
updatedText.addAttribute(.foregroundColor, value: self.textIsOptions ? self.theme.chat.inputPanel.secondaryTextColor : self.theme.chat.inputPanel.primaryTextColor, range: NSRange(location: 0, length: updatedText.length))
|
||||
self.textNode.attributedText = updatedText
|
||||
}
|
||||
self.textNode.spoilerColor = self.theme.chat.inputPanel.secondaryTextColor
|
||||
|
||||
|
@ -606,11 +606,11 @@ public final class CustomEmojiContainerView: UIView {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
public func update(fontSize: CGFloat, textColor: UIColor, emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) {
|
||||
public func update(fontSize: CGFloat, textColor: UIColor, emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute, CGFloat)]) {
|
||||
var nextIndexById: [Int64: Int] = [:]
|
||||
|
||||
var validKeys = Set<InlineStickerItemLayer.Key>()
|
||||
for (rect, emoji) in emojiRects {
|
||||
for (rect, emoji, fontSize) in emojiRects {
|
||||
let index: Int
|
||||
if let nextIndex = nextIndexById[emoji.fileId] {
|
||||
index = nextIndex
|
||||
|
@ -663,7 +663,7 @@ public final class TextFieldComponent: Component {
|
||||
}
|
||||
|
||||
var spoilerRects: [CGRect] = []
|
||||
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
|
||||
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute, CGFloat)] = []
|
||||
|
||||
let textView = self.textView
|
||||
if let attributedText = textView.attributedText {
|
||||
@ -707,9 +707,13 @@ public final class TextFieldComponent: Component {
|
||||
|
||||
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||
if let start = textView.position(from: beginning, offset: range.location), let end = textView.position(from: start, offset: range.length), let textRange = textView.textRange(from: start, to: end) {
|
||||
var emojiFontSize = component.fontSize
|
||||
if let font = attributes[.font] as? UIFont {
|
||||
emojiFontSize = font.pointSize
|
||||
}
|
||||
let textRects = textView.selectionRects(for: textRange)
|
||||
for textRect in textRects {
|
||||
customEmojiRects.append((textRect.rect, value))
|
||||
customEmojiRects.append((textRect.rect, value, emojiFontSize))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -132,6 +132,7 @@ public final class TextNodeWithEntities {
|
||||
let string = NSMutableAttributedString(attributedString: sourceString)
|
||||
|
||||
var fullRange = NSRange(location: 0, length: string.length)
|
||||
var originalTextId = 0
|
||||
while true {
|
||||
var found = false
|
||||
string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, stop in
|
||||
@ -141,7 +142,8 @@ public final class TextNodeWithEntities {
|
||||
let replacementRange = NSRange(location: 0, length: updatedSubstring.length)
|
||||
updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange)
|
||||
updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: replacementRange)
|
||||
updatedSubstring.addAttribute(originalTextAttributeKey, value: string.attributedSubstring(from: range).string, range: replacementRange)
|
||||
updatedSubstring.addAttribute(originalTextAttributeKey, value: OriginalTextAttribute(id: originalTextId, string: string.attributedSubstring(from: range).string), range: replacementRange)
|
||||
originalTextId += 1
|
||||
|
||||
let itemSize = (font.pointSize * 24.0 / 17.0)
|
||||
|
||||
|
@ -404,6 +404,7 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch
|
||||
}
|
||||
|
||||
if selfController.presentationInterfaceState.copyProtectionEnabled || messages.first?.isCopyProtected() == true {
|
||||
} else if messages.first?.id.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
} else {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Reply in Another Chat", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) }, action: { [weak selfController] c, f in
|
||||
@ -746,7 +747,17 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
|
||||
|
||||
if case let .Loaded(content) = linkOptions.webpage.content, let isMediaLargeByDefault = content.isMediaLargeByDefault, isMediaLargeByDefault {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: linkOptions.largeMedia ? "Shrink Photo" : "Enlarge Photo", icon: { theme in
|
||||
let shrinkTitle: String
|
||||
let enlargeTitle: String
|
||||
if let file = content.file, file.isVideo {
|
||||
shrinkTitle = "Shrink Video"
|
||||
enlargeTitle = "Enlarge Video"
|
||||
} else {
|
||||
shrinkTitle = "Shrink Photo"
|
||||
enlargeTitle = "Enlarge Photo"
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: linkOptions.largeMedia ? shrinkTitle : enlargeTitle, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: !linkOptions.largeMedia ? "Chat/Context Menu/ImageEnlarge" : "Chat/Context Menu/ImageShrink"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak selfController] _, f in
|
||||
selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||||
|
@ -3819,7 +3819,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}
|
||||
}
|
||||
if let messageId = message?.id, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||||
if let messageId = message?.id, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) ?? message {
|
||||
var quoteData: EngineMessageReplyQuote?
|
||||
|
||||
let nsRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound)
|
||||
@ -10765,9 +10765,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
|
||||
|
||||
let range = textInputState.selectionRange
|
||||
inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: text)
|
||||
|
||||
let selectionPosition = range.lowerBound + (text.string as NSString).length
|
||||
let updatedText = NSMutableAttributedString(attributedString: text)
|
||||
if range.lowerBound < inputText.length {
|
||||
if let quote = inputText.attribute(ChatTextInputAttributes.quote, at: range.lowerBound, effectiveRange: nil) {
|
||||
updatedText.addAttribute(ChatTextInputAttributes.quote, value: quote, range: NSRange(location: 0, length: updatedText.length))
|
||||
}
|
||||
}
|
||||
inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: updatedText)
|
||||
|
||||
let selectionPosition = range.lowerBound + (updatedText.string as NSString).length
|
||||
|
||||
return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode)
|
||||
}
|
||||
|
@ -3382,7 +3382,25 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
if let replyMessageSubject = self.chatPresentationInterfaceState.interfaceState.replyMessageSubject, let quote = replyMessageSubject.quote {
|
||||
if let replyMessage = self.chatPresentationInterfaceState.replyMessage {
|
||||
if !replyMessage.text.contains(quote.text) {
|
||||
let nsText = replyMessage.text as NSString
|
||||
var startIndex = 0
|
||||
var found = false
|
||||
while true {
|
||||
let range = nsText.range(of: quote.text, range: NSRange(location: startIndex, length: nsText.length - startIndex))
|
||||
if range.location != NSNotFound {
|
||||
let subEntities = messageTextEntitiesInRange(entities: replyMessage.textEntitiesAttribute?.entities ?? [], range: range, onlyQuoteable: true)
|
||||
if subEntities == quote.entities {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
startIndex = range.upperBound
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
//TODO:localize
|
||||
let authorName: String = (replyMessage.author.flatMap(EnginePeer.init))?.compactDisplayTitle ?? ""
|
||||
let errorTextData = self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedText(authorName)
|
||||
|
@ -2572,7 +2572,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
|
||||
let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
|
||||
|
||||
var rects: [CGRect] = []
|
||||
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
|
||||
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute, CGFloat)] = []
|
||||
|
||||
let fontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
||||
|
||||
@ -2619,7 +2619,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
|
||||
if let start = textInputNode.textView.position(from: beginning, offset: range.location), let end = textInputNode.textView.position(from: start, offset: range.length), let textRange = textInputNode.textView.textRange(from: start, to: end) {
|
||||
let textRects = textInputNode.textView.selectionRects(for: textRange)
|
||||
for textRect in textRects {
|
||||
customEmojiRects.append((textRect.rect, value))
|
||||
var emojiFontSize = fontSize
|
||||
if let font = attributes[.font] as? UIFont {
|
||||
emojiFontSize = font.pointSize
|
||||
}
|
||||
customEmojiRects.append((textRect.rect, value, emojiFontSize))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,15 @@ public struct ChatTextInputAttributes {
|
||||
}
|
||||
|
||||
public let originalTextAttributeKey = NSAttributedString.Key(rawValue: "Attribute__OriginalText")
|
||||
public final class OriginalTextAttribute: NSObject {
|
||||
public let id: Int
|
||||
public let string: String
|
||||
|
||||
public init(id: Int, string: String) {
|
||||
self.id = id
|
||||
self.string = string
|
||||
}
|
||||
}
|
||||
|
||||
public func stateAttributedStringForText(_ text: NSAttributedString) -> NSAttributedString {
|
||||
let sourceString = NSMutableAttributedString(attributedString: text)
|
||||
|
@ -446,6 +446,11 @@ public final class TextSelectionNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
public func setSelection(range: NSRange, displayMenu: Bool) {
|
||||
guard let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else {
|
||||
return
|
||||
}
|
||||
let range = self.convertSelectionFromOriginalText(attributedString: attributedString, range: range)
|
||||
|
||||
self.currentRange = (range.lowerBound, range.upperBound)
|
||||
self.updateSelection(range: range, animateIn: true)
|
||||
self.updateIsActive(true)
|
||||
@ -455,12 +460,109 @@ public final class TextSelectionNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
private func convertSelectionToOriginalText(attributedString: NSAttributedString, range: NSRange) -> NSRange {
|
||||
var adjustedRange = range
|
||||
|
||||
do {
|
||||
attributedString.enumerateAttribute(originalTextAttributeKey, in: NSRange(location: 0, length: range.lowerBound), options: [], using: { value, range, stop in
|
||||
guard let value = value as? OriginalTextAttribute else {
|
||||
return
|
||||
}
|
||||
let updatedSubstring = NSMutableAttributedString(string: value.string)
|
||||
let difference = updatedSubstring.length - range.length
|
||||
|
||||
adjustedRange.location += difference
|
||||
})
|
||||
}
|
||||
|
||||
do {
|
||||
attributedString.enumerateAttribute(originalTextAttributeKey, in: range, options: [], using: { value, range, stop in
|
||||
guard let value = value as? OriginalTextAttribute else {
|
||||
return
|
||||
}
|
||||
let updatedSubstring = NSMutableAttributedString(string: value.string)
|
||||
let difference = updatedSubstring.length - range.length
|
||||
|
||||
adjustedRange.length += difference
|
||||
})
|
||||
}
|
||||
|
||||
return adjustedRange
|
||||
}
|
||||
|
||||
private func convertSelectionFromOriginalText(attributedString: NSAttributedString, range: NSRange) -> NSRange {
|
||||
var adjustedRange = range
|
||||
|
||||
final class PreviousText: NSObject {
|
||||
let id: Int
|
||||
let string: String
|
||||
|
||||
init(id: Int, string: String) {
|
||||
self.id = id
|
||||
self.string = string
|
||||
}
|
||||
}
|
||||
|
||||
var nextId = 0
|
||||
let attributedString = NSMutableAttributedString(attributedString: attributedString)
|
||||
var fullRange = NSRange(location: 0, length: attributedString.length)
|
||||
while true {
|
||||
var found = false
|
||||
attributedString.enumerateAttribute(originalTextAttributeKey, in: fullRange, options: [], using: { value, range, stop in
|
||||
if let value = value as? OriginalTextAttribute {
|
||||
let updatedSubstring = NSMutableAttributedString(string: value.string)
|
||||
|
||||
let replacementRange = NSRange(location: 0, length: updatedSubstring.length)
|
||||
updatedSubstring.addAttributes(attributedString.attributes(at: range.location, effectiveRange: nil), range: replacementRange)
|
||||
updatedSubstring.addAttribute(NSAttributedString.Key(rawValue: "__previous_text"), value: PreviousText(id: nextId, string: attributedString.attributedSubstring(from: range).string), range: replacementRange)
|
||||
nextId += 1
|
||||
|
||||
attributedString.replaceCharacters(in: range, with: updatedSubstring)
|
||||
let updatedRange = NSRange(location: range.location, length: updatedSubstring.length)
|
||||
|
||||
found = true
|
||||
stop.pointee = ObjCBool(true)
|
||||
fullRange = NSRange(location: updatedRange.upperBound, length: fullRange.upperBound - range.upperBound)
|
||||
}
|
||||
})
|
||||
if !found {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
attributedString.enumerateAttribute(NSAttributedString.Key(rawValue: "__previous_text"), in: NSRange(location: 0, length: range.lowerBound), options: [], using: { value, range, stop in
|
||||
guard let value = value as? PreviousText else {
|
||||
return
|
||||
}
|
||||
let updatedSubstring = NSMutableAttributedString(string: value.string)
|
||||
let difference = updatedSubstring.length - range.length
|
||||
|
||||
adjustedRange.location += difference
|
||||
})
|
||||
}
|
||||
|
||||
do {
|
||||
attributedString.enumerateAttribute(NSAttributedString.Key(rawValue: "__previous_text"), in: range, options: [], using: { value, range, stop in
|
||||
guard let value = value as? PreviousText else {
|
||||
return
|
||||
}
|
||||
let updatedSubstring = NSMutableAttributedString(string: value.string)
|
||||
let difference = updatedSubstring.length - range.length
|
||||
|
||||
adjustedRange.length += difference
|
||||
})
|
||||
}
|
||||
|
||||
return adjustedRange
|
||||
}
|
||||
|
||||
public func getSelection() -> NSRange? {
|
||||
guard let currentRange = self.currentRange else {
|
||||
guard let currentRange = self.currentRange, let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else {
|
||||
return nil
|
||||
}
|
||||
let range = NSRange(location: min(currentRange.0, currentRange.1), length: max(currentRange.0, currentRange.1) - min(currentRange.0, currentRange.1))
|
||||
return range
|
||||
return self.convertSelectionToOriginalText(attributedString: attributedString, range: range)
|
||||
}
|
||||
|
||||
private func updateSelection(range: NSRange?, animateIn: Bool) {
|
||||
@ -578,8 +680,8 @@ public final class TextSelectionNode: ASDisplayNode {
|
||||
while true {
|
||||
var found = false
|
||||
string.enumerateAttribute(originalTextAttributeKey, in: fullRange, options: [], using: { value, range, stop in
|
||||
if let value = value as? String {
|
||||
let updatedSubstring = NSMutableAttributedString(string: value)
|
||||
if let value = value as? OriginalTextAttribute {
|
||||
let updatedSubstring = NSMutableAttributedString(string: value.string)
|
||||
|
||||
let replacementRange = NSRange(location: 0, length: updatedSubstring.length)
|
||||
updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange)
|
||||
@ -597,6 +699,8 @@ public final class TextSelectionNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
let adjustedRange = self.convertSelectionToOriginalText(attributedString: attributedString, range: range)
|
||||
|
||||
var actions: [ContextMenuAction] = []
|
||||
if self.enableCopy {
|
||||
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
|
||||
@ -606,7 +710,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
||||
}
|
||||
if self.enableQuote {
|
||||
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuQuote, accessibilityLabel: self.strings.Conversation_ContextMenuQuote), action: { [weak self] in
|
||||
self?.performAction(string, .quote(range: range.lowerBound ..< range.upperBound))
|
||||
self?.performAction(string, .quote(range: adjustedRange.lowerBound ..< adjustedRange.upperBound))
|
||||
self?.cancelSelection()
|
||||
}))
|
||||
} else if self.enableLookup {
|
||||
|
Loading…
x
Reference in New Issue
Block a user