diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index 726dc5aa23..fd3be8f1e2 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -338,7 +338,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider) + textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) self.updatingInputState = false self.updateTextNodeText(animated: animated) @@ -1020,7 +1022,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS public func chatInputTextNodeDidUpdateText() { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) self.updateSpoiler() @@ -1192,9 +1196,13 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS textInputNode.textView.isScrollEnabled = false - refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) - textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider) + textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) if textInputNode.textView.subviews.count > 1, animated { let containerView = textInputNode.textView.subviews[1] @@ -1349,7 +1357,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS let textFont = Font.regular(baseFontSize) let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor - let attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider) + let attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) let range = (attributedText.string as NSString).range(of: "\n") if range.location != NSNotFound { @@ -1754,7 +1764,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider) + let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) string.replaceCharacters(in: range, with: cleanReplacementString) self.textInputNode?.attributedText = string self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0) diff --git a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift b/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift index 8179f8b295..da7d687862 100644 --- a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift +++ b/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift @@ -228,7 +228,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe rightInset += inlineAction.icon.size.width + 8.0 } - let itemText = textAttributedStringForStateText(item.text, fontSize: 17.0, textColor: item.presentationData.theme.chat.inputPanel.primaryTextColor, accentTextColor: item.presentationData.theme.chat.inputPanel.panelControlAccentColor, writingDirection: nil, spoilersRevealed: false, availableEmojis: Set(), emojiViewProvider: nil) + let itemText = textAttributedStringForStateText(item.text, fontSize: 17.0, textColor: item.presentationData.theme.chat.inputPanel.primaryTextColor, accentTextColor: item.presentationData.theme.chat.inputPanel.panelControlAccentColor, writingDirection: nil, spoilersRevealed: false, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil) let measureText = NSMutableAttributedString(attributedString: itemText) let measureRawString = measureText.string if measureRawString.hasSuffix("\n") || measureRawString.isEmpty { @@ -294,11 +294,11 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe if let currentText = strongSelf.textNode.attributedText { if currentText.string != attributedText.string || updatedTheme != nil { strongSelf.textNode.attributedText = attributedText - refreshGenericTextInputAttributes(strongSelf.textNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) + refreshGenericTextInputAttributes(strongSelf.textNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil) } } else { strongSelf.textNode.attributedText = attributedText - refreshGenericTextInputAttributes(strongSelf.textNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) + refreshGenericTextInputAttributes(strongSelf.textNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil) } if strongSelf.backgroundNode.supernode == nil { @@ -591,7 +591,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { if let item = self.item { if let _ = self.textNode.attributedText { - refreshGenericTextInputAttributes(editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) + refreshGenericTextInputAttributes(editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil) let updatedText = stateAttributedStringForText(self.textNode.attributedText!) item.textUpdated(updatedText) } else { @@ -621,7 +621,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe } refreshChatTextInputTypingAttributes(editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0) - refreshGenericTextInputAttributes(editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) + refreshGenericTextInputAttributes(editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil) } } @@ -672,7 +672,7 @@ private func chatTextInputAddFormattingAttribute(item: CreatePollTextInputItem, textNode.selectedRange = nsRange refreshChatTextInputTypingAttributes(textNode.textView, theme: theme, baseFontSize: 17.0) - refreshGenericTextInputAttributes(textNode.textView, theme: theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) + refreshGenericTextInputAttributes(textNode.textView, theme: theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil) let updatedText = stateAttributedStringForText(textNode.attributedText!) item.textUpdated(updatedText) diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift index 0a0b3ae3d3..3026167bca 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift @@ -400,13 +400,15 @@ private final class ChatInputLegacyLayoutManager: NSLayoutManager { private struct DisplayBlockQuote { var id: Int var boundingRect: CGRect - var attribute: ChatTextInputTextQuoteAttribute + var kind: ChatTextInputTextQuoteAttribute.Kind + var isCollapsed: Bool var range: NSRange - init(id: Int, boundingRect: CGRect, attribute: ChatTextInputTextQuoteAttribute, range: NSRange) { + init(id: Int, boundingRect: CGRect, kind: ChatTextInputTextQuoteAttribute.Kind, isCollapsed: Bool, range: NSRange) { self.id = id self.boundingRect = boundingRect - self.attribute = attribute + self.kind = kind + self.isCollapsed = isCollapsed self.range = range } } @@ -612,7 +614,50 @@ private final class ChatInputTextLegacyInternal: NSObject, ChatInputTextInternal boundingRect.origin.y -= 4.0 boundingRect.size.height += 8.0 - result.append(DisplayBlockQuote(id: id, boundingRect: boundingRect, attribute: value, range: range)) + result.append(DisplayBlockQuote(id: id, boundingRect: boundingRect, kind: value.kind, isCollapsed: value.isCollapsed, range: range)) + + blockQuoteIndex += 1 + } + }) + self.customTextStorage.enumerateAttribute(NSAttributedString.Key.attachment, in: NSRange(location: 0, length: self.customTextStorage.length), using: { value, range, _ in + if let _ = value as? ChatInputTextCollapsedQuoteAttachment { + let glyphRange = self.customLayoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil) + if self.customLayoutManager.isValidGlyphIndex(glyphRange.location) && self.customLayoutManager.isValidGlyphIndex(glyphRange.location + glyphRange.length - 1) { + } else { + return + } + + let id = blockQuoteIndex + + var boundingRect = CGRect() + var startIndex = glyphRange.lowerBound + while startIndex < glyphRange.upperBound { + var effectiveRange = NSRange(location: NSNotFound, length: 0) + let rect = self.customLayoutManager.lineFragmentUsedRect(forGlyphAt: startIndex, effectiveRange: &effectiveRange) + if boundingRect.isEmpty { + boundingRect = rect + } else { + boundingRect = boundingRect.union(rect) + } + if effectiveRange.location != NSNotFound { + startIndex = max(startIndex + 1, effectiveRange.upperBound) + } else { + break + } + } + + boundingRect.origin.y += self.defaultTextContainerInset.top + + boundingRect.origin.x += 5.0 + boundingRect.size.width += 4.0 + boundingRect.size.width += 18.0 + boundingRect.size.width = min(boundingRect.size.width, self.textContainer.size.width - 18.0) + boundingRect.size.width = min(boundingRect.size.width, self.textContainer.size.width) + + boundingRect.origin.y += 4.0 + boundingRect.size.height -= 8.0 + + result.append(DisplayBlockQuote(id: id, boundingRect: boundingRect, kind: .quote, isCollapsed: true, range: range)) blockQuoteIndex += 1 } @@ -735,7 +780,7 @@ private final class ChatInputTextNewInternal: NSObject, ChatInputTextInternal, N } else { let id = nextId nextId += 1 - result[quoteId] = DisplayBlockQuote(id: id, boundingRect: fragmentFrame, attribute: attribute, range: fragmentRange) + result[quoteId] = DisplayBlockQuote(id: id, boundingRect: fragmentFrame, kind: attribute.kind, isCollapsed: attribute.isCollapsed, range: fragmentRange) } } } @@ -749,6 +794,116 @@ private final class ChatInputTextNewInternal: NSObject, ChatInputTextInternal, N } } +private let registeredViewProvider: Void = { + if #available(iOS 15.0, *) { + NSTextAttachment.registerViewProviderClass(ChatInputTextCollapsedQuoteAttachmentImpl.ViewProvider.self, forFileType: "public.data") + } +}() + +public final class ChatInputTextCollapsedQuoteAttachmentImpl: NSTextAttachment, ChatInputTextCollapsedQuoteAttachment { + final class View: UIView { + let attachment: ChatInputTextCollapsedQuoteAttachmentImpl + let textNode: ImmediateTextNode + + init(attachment: ChatInputTextCollapsedQuoteAttachmentImpl) { + self.attachment = attachment + self.textNode = ImmediateTextNode() + self.textNode.maximumNumberOfLines = 3 + + super.init(frame: CGRect()) + + self.addSubview(self.textNode.view) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + static func calculateSize(attachment: ChatInputTextCollapsedQuoteAttachmentImpl, constrainedSize: CGSize) -> CGSize { + let renderingText = NSMutableAttributedString(attributedString: attachment.text) + renderingText.addAttribute(.font, value: attachment.attributes.font, range: NSRange(location: 0, length: renderingText.length)) + renderingText.addAttribute(.foregroundColor, value: attachment.attributes.textColor, range: NSRange(location: 0, length: renderingText.length)) + + let textNode = ImmediateTextNode() + textNode.maximumNumberOfLines = 3 + + textNode.attributedText = renderingText + textNode.cutout = TextNodeCutout(topRight: CGSize(width: 40.0, height: 10.0)) + + let layoutInfo = textNode.updateLayoutFullInfo(CGSize(width: constrainedSize.width - 9.0, height: constrainedSize.height)) + + return CGSize(width: constrainedSize.width, height: 8.0 + layoutInfo.size.height + 8.0) + } + + override func layoutSubviews() { + super.layoutSubviews() + + let renderingText = NSMutableAttributedString(attributedString: attachment.text) + renderingText.addAttribute(.font, value: attachment.attributes.font, range: NSRange(location: 0, length: renderingText.length)) + renderingText.addAttribute(.foregroundColor, value: attachment.attributes.textColor, range: NSRange(location: 0, length: renderingText.length)) + + self.textNode.attributedText = renderingText + self.textNode.cutout = TextNodeCutout(topRight: CGSize(width: 10.0, height: 8.0)) + + let maxTextSize = CGSize(width: self.bounds.size.width - 9.0, height: self.bounds.size.height) + let layoutInfo = self.textNode.updateLayoutFullInfo(maxTextSize) + + self.textNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 8.0), size: layoutInfo.size) + } + } + + @available(iOS 15.0, *) + final class ViewProvider: NSTextAttachmentViewProvider { + override init( + textAttachment: NSTextAttachment, + parentView: UIView?, + textLayoutManager: NSTextLayoutManager?, + location: NSTextLocation + ) { + super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location) + } + + override public func loadView() { + if let textAttachment = self.textAttachment as? ChatInputTextCollapsedQuoteAttachmentImpl { + self.view = View(attachment: textAttachment) + } else { + self.view = UIView() + } + } + } + + public let text: NSAttributedString + public let attributes: ChatInputTextCollapsedQuoteAttributes + + public init(text: NSAttributedString, attributes: ChatInputTextCollapsedQuoteAttributes) { + let _ = registeredViewProvider + + self.text = text + self.attributes = attributes + + super.init(data: nil, ofType: "public.data") + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func renderingText() -> NSAttributedString { + let result = NSMutableAttributedString(attributedString: self.text) + result.addAttribute(.font, value: self.attributes.font, range: NSRange(location: 0, length: result.length)) + result.addAttribute(.foregroundColor, value: self.attributes.textColor, range: NSRange(location: 0, length: result.length)) + return result + } + + override public func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect { + return CGRect(origin: CGPoint(), size: View.calculateSize(attachment: self, constrainedSize: CGSize(width: lineFrag.width, height: 10000.0))) + } + + override public func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> UIImage? { + return nil + } +} + public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate, NSLayoutManagerDelegate, NSTextStorageDelegate { public final class Theme: Equatable { public final class Quote: Equatable { @@ -1161,7 +1316,7 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate, blockQuote.frame = displayBlockQuote.boundingRect if let theme = self.theme { - blockQuote.update(value: displayBlockQuote.attribute, range: displayBlockQuote.range, size: displayBlockQuote.boundingRect.size, theme: theme.quote) + blockQuote.update(kind: displayBlockQuote.kind, isCollapsed: displayBlockQuote.isCollapsed, range: displayBlockQuote.range, size: displayBlockQuote.boundingRect.size, theme: theme.quote) } validBlockQuotes.append(displayBlockQuote.id) @@ -1255,7 +1410,7 @@ private final class QuoteBackgroundView: UIView { } } - func update(value: ChatTextInputTextQuoteAttribute, range: NSRange, size: CGSize, theme: ChatInputTextView.Theme.Quote) { + func update(kind: ChatTextInputTextQuoteAttribute.Kind, isCollapsed: Bool, range: NSRange, size: CGSize, theme: ChatInputTextView.Theme.Quote) { self.range = range if self.theme != theme { @@ -1270,7 +1425,7 @@ private final class QuoteBackgroundView: UIView { let collapseButtonSize = CGSize(width: 18.0, height: 18.0) self.collapseButton.frame = CGRect(origin: CGPoint(x: size.width - 2.0 - collapseButtonSize.width, y: 2.0), size: collapseButtonSize) - if value.isCollapsed { + if isCollapsed { self.collapseButtonIconView.image = quoteExpandImage } else { self.collapseButtonIconView.image = quoteCollapseImage @@ -1285,9 +1440,9 @@ private final class QuoteBackgroundView: UIView { var tertiaryColor: UIColor? let backgroundColor: UIColor? - switch value.kind { + switch kind { case .quote: - if size.height >= 100.0 { + if size.height >= 100.0 || isCollapsed { self.iconView.isHidden = true self.collapseButton.isHidden = false } else { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift index 76d65ff36a..950f58bc9b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift @@ -7,13 +7,12 @@ import ChatMessageItemCommon public func chatMessageBubbleImageContentCorners(relativeContentPosition position: ChatMessageBubbleContentPosition, normalRadius: CGFloat, mergedRadius: CGFloat, mergedWithAnotherContentRadius: CGFloat, layoutConstants: ChatMessageItemLayoutConstants, chatPresentationData: ChatPresentationData) -> ImageCorners { let topLeftCorner: ImageCorner let topRightCorner: ImageCorner - switch position { case let .linear(top, _): switch top { case .Neighbour: - topLeftCorner = .Corner(normalRadius) - topRightCorner = .Corner(normalRadius) + topLeftCorner = .Corner(mergedWithAnotherContentRadius) + topRightCorner = .Corner(mergedWithAnotherContentRadius) case .BubbleNeighbour: topLeftCorner = .Corner(mergedRadius) topRightCorner = .Corner(mergedRadius) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 5724c17098..42dc5e13c8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -111,7 +111,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private var codeHighlightState: (id: EngineMessage.Id, specs: [CachedMessageSyntaxHighlight.Spec], disposable: Disposable)? - private var collapsedBlockIds: Set = Set() + private var expandedBlockIds: Set = Set() override public var visibility: ListViewItemNodeVisibility { didSet { @@ -155,13 +155,25 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { guard let self, let item = self.item else { return } - if self.collapsedBlockIds.contains(blockId) { - self.collapsedBlockIds.remove(blockId) + if self.expandedBlockIds.contains(blockId) { + self.expandedBlockIds.remove(blockId) } else { - self.collapsedBlockIds.insert(blockId) + self.expandedBlockIds.insert(blockId) } item.controllerInteraction.requestMessageUpdate(item.message.id, false) } + self.textNode.textNode.canHandleTapAtPoint = { [weak self] point in + guard let self else { + return false + } + let localPoint = self.textNode.textNode.view.convert(point, to: self.view) + let action = self.tapActionAtPoint(localPoint, gesture: .tap, isEstimating: true) + if case .none = action.content { + return true + } else { + return false + } + } } required public init?(coder aDecoder: NSCoder) { @@ -179,7 +191,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let statusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.statusNode) let currentCachedChatMessageText = self.cachedChatMessageText - let collapsedBlockIds = self.collapsedBlockIds + let expandedBlockIds = self.expandedBlockIds return { item, layoutConstants, _, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) @@ -434,7 +446,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var codeHighlightSpecs: [CachedMessageSyntaxHighlight.Spec] = [] var cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? - if let entities = entities { + if let entities { var underlineLinks = true if !messageTheme.primaryTextColor.isEqual(messageTheme.linkTextColor) { underlineLinks = false @@ -569,7 +581,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { lineColor: messageTheme.accentControlColor, displayContentsUnderSpoilers: false, customTruncationToken: customTruncationToken, - collapsedBlocks: collapsedBlockIds + expandedBlocks: expandedBlockIds )) var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))? diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index 4035454d1c..7340cfbc37 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -271,7 +271,7 @@ public final class InteractiveTextNodeLayoutArguments { public let textStroke: (UIColor, CGFloat)? public let displayContentsUnderSpoilers: Bool public let customTruncationToken: NSAttributedString? - public let collapsedBlocks: Set + public let expandedBlocks: Set public init( attributedString: NSAttributedString?, @@ -291,7 +291,7 @@ public final class InteractiveTextNodeLayoutArguments { textStroke: (UIColor, CGFloat)? = nil, displayContentsUnderSpoilers: Bool = false, customTruncationToken: NSAttributedString? = nil, - collapsedBlocks: Set = Set() + expandedBlocks: Set = Set() ) { self.attributedString = attributedString self.backgroundColor = backgroundColor @@ -310,7 +310,7 @@ public final class InteractiveTextNodeLayoutArguments { self.textStroke = textStroke self.displayContentsUnderSpoilers = displayContentsUnderSpoilers self.customTruncationToken = customTruncationToken - self.collapsedBlocks = collapsedBlocks + self.expandedBlocks = expandedBlocks } public func withAttributedString(_ attributedString: NSAttributedString?) -> InteractiveTextNodeLayoutArguments { @@ -332,7 +332,7 @@ public final class InteractiveTextNodeLayoutArguments { textStroke: self.textStroke, displayContentsUnderSpoilers: self.displayContentsUnderSpoilers, customTruncationToken: self.customTruncationToken, - collapsedBlocks: self.collapsedBlocks + expandedBlocks: self.expandedBlocks ) } } @@ -394,7 +394,7 @@ public final class InteractiveTextNodeLayout: NSObject { fileprivate let textShadowBlur: CGFloat? fileprivate let textStroke: (UIColor, CGFloat)? fileprivate let displayContentsUnderSpoilers: Bool - fileprivate let collapsedBlocks: Set + fileprivate let expandedBlocks: Set fileprivate init( attributedString: NSAttributedString?, @@ -418,7 +418,7 @@ public final class InteractiveTextNodeLayout: NSObject { textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displayContentsUnderSpoilers: Bool, - collapsedBlocks: Set + expandedBlocks: Set ) { self.attributedString = attributedString self.maximumNumberOfLines = maximumNumberOfLines @@ -441,7 +441,7 @@ public final class InteractiveTextNodeLayout: NSObject { self.textShadowBlur = textShadowBlur self.textStroke = textStroke self.displayContentsUnderSpoilers = displayContentsUnderSpoilers - self.collapsedBlocks = collapsedBlocks + self.expandedBlocks = expandedBlocks } public var numberOfLines: Int { @@ -1048,7 +1048,7 @@ private func addAttachment(attachment: UIImage, line: InteractiveTextNodeLine, a line.attachments.append(InteractiveTextNodeAttachment(range: NSMakeRange(startIndex, endIndex - startIndex), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), attachment: attachment)) } -open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol { +open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecognizerDelegate { public struct RenderContentTypes: OptionSet { public var rawValue: Int @@ -1078,6 +1078,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol { public var renderContentTypes: RenderContentTypes = .all private var contentItemLayers: [Int: TextContentItemLayer] = [:] + public var canHandleTapAtPoint: ((CGPoint) -> Bool)? public var requestToggleBlockCollapsed: ((Int) -> Void)? private var tapRecognizer: UITapGestureRecognizer? @@ -1138,6 +1139,13 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol { } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let canHandleTapAtPoint = self.canHandleTapAtPoint else { + return nil + } + if !canHandleTapAtPoint(point) { + return nil + } + guard let result = super.hitTest(point, with: event) else { return nil } @@ -1198,7 +1206,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol { textStroke: (UIColor, CGFloat)?, displayContentsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?, - collapsedBlocks: Set + expandedBlocks: Set ) -> InteractiveTextNodeLayout { let blockQuoteLeftInset: CGFloat = 9.0 let blockQuoteRightInset: CGFloat = 0.0 @@ -1459,7 +1467,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol { blockIndex = blockIndexValue nextBlockIndex += 1 if blockQuote.isCollapsible { - isCollapsed = collapsedBlocks.contains(blockIndexValue) + isCollapsed = !expandedBlocks.contains(blockIndexValue) } } @@ -1550,16 +1558,16 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol { if firstLineOffset == nil, let firstLine = segmentLines.first { firstLineOffset = firstLine.descent } - - if !isCollapsed, let blockQuote = segment.blockQuote, blockQuote.isCollapsible, !segment.lines.isEmpty { - let lastLine = segment.lines[segment.lines.count - 1] - if lastLine.frame.maxX + 16.0 <= constrainedSize.width { - lastLine.frame.size.width += 16.0 - blockWidth = max(blockWidth, lastLine.frame.maxX) - } else { - segmentHeight += 10.0 - effectiveSegmentHeight += 10.0 - } + } + + if !isCollapsed, let blockQuote = segment.blockQuote, blockQuote.isCollapsible, !segment.lines.isEmpty { + let lastLine = segment.lines[segment.lines.count - 1] + if lastLine.frame.maxX + 16.0 <= constrainedSize.width { + lastLine.frame.size.width += 16.0 + blockWidth = max(blockWidth, lastLine.frame.maxX) + } else { + segmentHeight += 10.0 + effectiveSegmentHeight += 10.0 } } @@ -1626,16 +1634,16 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol { textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, - collapsedBlocks: collapsedBlocks + expandedBlocks: expandedBlocks ) } - static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displayContentsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?, collapsedBlocks: Set) -> InteractiveTextNodeLayout { + static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displayContentsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?, expandedBlocks: Set) -> InteractiveTextNodeLayout { guard let attributedString else { - return InteractiveTextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, segments: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, collapsedBlocks: collapsedBlocks) + return InteractiveTextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, segments: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, expandedBlocks: expandedBlocks) } - return calculateLayoutV2(attributedString: attributedString, minimumNumberOfLines: minimumNumberOfLines, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, verticalAlignment: verticalAlignment, lineSpacingFactor: lineSpacingFactor, cutout: cutout, insets: insets, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, customTruncationToken: customTruncationToken, collapsedBlocks: collapsedBlocks) + return calculateLayoutV2(attributedString: attributedString, minimumNumberOfLines: minimumNumberOfLines, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, verticalAlignment: verticalAlignment, lineSpacingFactor: lineSpacingFactor, cutout: cutout, insets: insets, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, customTruncationToken: customTruncationToken, expandedBlocks: expandedBlocks) } private func updateContentItems(animation: ListViewItemUpdateAnimation) { @@ -1716,6 +1724,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol { let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGesture(_:))) self.tapRecognizer = tapRecognizer self.view.addGestureRecognizer(tapRecognizer) + tapRecognizer.delegate = self } } else if let tapRecognizer = self.tapRecognizer { self.tapRecognizer = nil @@ -1723,6 +1732,10 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol { } } + public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { let point = recognizer.location(in: self.view) @@ -1738,7 +1751,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol { return { arguments in let layout: InteractiveTextNodeLayout - if let existingLayout = existingLayout, existingLayout.constrainedSize == arguments.constrainedSize && existingLayout.maximumNumberOfLines == arguments.maximumNumberOfLines && existingLayout.truncationType == arguments.truncationType && existingLayout.cutout == arguments.cutout && existingLayout.explicitAlignment == arguments.alignment && existingLayout.lineSpacing.isEqual(to: arguments.lineSpacing) && existingLayout.collapsedBlocks == arguments.collapsedBlocks { + if let existingLayout = existingLayout, existingLayout.constrainedSize == arguments.constrainedSize && existingLayout.maximumNumberOfLines == arguments.maximumNumberOfLines && existingLayout.truncationType == arguments.truncationType && existingLayout.cutout == arguments.cutout && existingLayout.explicitAlignment == arguments.alignment && existingLayout.lineSpacing.isEqual(to: arguments.lineSpacing) && existingLayout.expandedBlocks == arguments.expandedBlocks { let stringMatch: Bool var colorMatch: Bool = true @@ -1763,10 +1776,10 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol { if stringMatch { layout = existingLayout } else { - layout = InteractiveTextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displayContentsUnderSpoilers: arguments.displayContentsUnderSpoilers, customTruncationToken: arguments.customTruncationToken, collapsedBlocks: arguments.collapsedBlocks) + layout = InteractiveTextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displayContentsUnderSpoilers: arguments.displayContentsUnderSpoilers, customTruncationToken: arguments.customTruncationToken, expandedBlocks: arguments.expandedBlocks) } } else { - layout = InteractiveTextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displayContentsUnderSpoilers: arguments.displayContentsUnderSpoilers, customTruncationToken: arguments.customTruncationToken, collapsedBlocks: arguments.collapsedBlocks) + layout = InteractiveTextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displayContentsUnderSpoilers: arguments.displayContentsUnderSpoilers, customTruncationToken: arguments.customTruncationToken, expandedBlocks: arguments.expandedBlocks) } let node = maybeNode ?? InteractiveTextNode() diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 4ac5a12927..2a4885a16d 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -314,13 +314,17 @@ public final class TextFieldComponent: Component { let inputState = f(self.inputState) let currentAttributedText = self.textView.attributedText - let updatedAttributedText = textAttributedStringForStateText(inputState.inputText, fontSize: component.fontSize, textColor: component.textColor, accentTextColor: component.accentColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider) + let updatedAttributedText = textAttributedStringForStateText(inputState.inputText, fontSize: component.fontSize, textColor: component.textColor, accentTextColor: component.accentColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) if currentAttributedText != updatedAttributedText { self.textView.attributedText = updatedAttributedText } self.textView.selectedRange = NSMakeRange(inputState.selectionRange.lowerBound, inputState.selectionRange.count) - refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.accentColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.accentColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) self.updateEntities() @@ -452,7 +456,9 @@ public final class TextFieldComponent: Component { guard let component = self.component else { return } - refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.accentColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.accentColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) refreshChatTextInputTypingAttributes(self.textView, textColor: component.textColor, baseFontSize: component.fontSize) self.textView.updateTextContainerInset() @@ -957,7 +963,9 @@ public final class TextFieldComponent: Component { self.textView.isScrollEnabled = false - refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.accentColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.accentColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) refreshChatTextInputTypingAttributes(self.textView, textColor: component.textColor, baseFontSize: component.fontSize) if self.textView.subviews.count > 1, animated { diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 3bb11d3ecb..74d6546447 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -731,11 +731,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) + textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) if let presentationInterfaceState = self.presentationInterfaceState { - refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) } self.updatingInputState = false @@ -1205,15 +1209,45 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch guard let self else { return } + guard let presentationInterfaceState = self.presentationInterfaceState else { + return + } + + let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor + let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in - let result = NSMutableAttributedString(attributedString: current.inputText) + let textAttributedString = textAttributedStringForStateText( + current.inputText, + fontSize: baseFontSize, + textColor: textColor, + accentTextColor: accentTextColor, + writingDirection: nil, + spoilersRevealed: self.spoilersRevealed, + availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), + emojiViewProvider: self.emojiViewProvider, + makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + } + ) + let result = NSMutableAttributedString(attributedString: textAttributedString) - if let current = result.attribute(ChatTextInputAttributes.block, at: range.lowerBound, effectiveRange: nil) as? ChatTextInputTextQuoteAttribute { - result.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: current.kind, isCollapsed: !current.isCollapsed), range: range) + var effectiveRange: NSRange = NSRange(location: NSNotFound, length: 0) + if let current = result.attribute(ChatTextInputAttributes.block, at: range.lowerBound, effectiveRange: &effectiveRange) as? ChatTextInputTextQuoteAttribute { + result.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: current.kind, isCollapsed: !current.isCollapsed), range: effectiveRange) + } else if let current = result.attribute(.attachment, at: range.lowerBound, effectiveRange: &effectiveRange) as? ChatInputTextCollapsedQuoteAttachment { + result.replaceCharacters(in: effectiveRange, with: "") + let updatedQuote = NSMutableAttributedString(attributedString: current.text) + updatedQuote.removeAttribute(ChatTextInputAttributes.block, range: NSRange(location: 0, length: updatedQuote.length)) + updatedQuote.addAttribute(ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false), range: NSRange(location: 0, length: updatedQuote.length)) + result.insert(updatedQuote, at: effectiveRange.lowerBound) } + let stateResult = stateAttributedStringForText(result) + return (ChatTextInputState( - inputText: result, + inputText: stateResult, selectionRange: current.selectionRange ), inputMode) } @@ -2912,7 +2946,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch func chatInputTextNodeDidUpdateText() { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) self.updateSpoiler() @@ -3082,9 +3118,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch textInputNode.textView.isScrollEnabled = false - refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) - textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) + textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) if textInputNode.textView.subviews.count > 1, animated { let containerView = textInputNode.textView.subviews[1] @@ -4340,7 +4380,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) + let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) string.replaceCharacters(in: range, with: cleanReplacementString) self.textInputNode?.attributedText = string self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0) diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index cdabbbf67e..d9f0b154fd 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -35,6 +35,37 @@ public final class OriginalTextAttribute: NSObject { } } +public final class ChatInputTextCollapsedQuoteAttributes: Equatable { + public let font: UIFont + public let textColor: UIColor + + public init( + font: UIFont, + textColor: UIColor + ) { + self.font = font + self.textColor = textColor + } + + public static func ==(lhs: ChatInputTextCollapsedQuoteAttributes, rhs: ChatInputTextCollapsedQuoteAttributes) -> Bool { + if lhs === rhs { + return true + } + if !lhs.font.isEqual(rhs.font) { + return false + } + if !lhs.textColor.isEqual(rhs.textColor) { + return false + } + + return true + } +} + +public protocol ChatInputTextCollapsedQuoteAttachment: NSTextAttachment { + var text: NSAttributedString { get } +} + public func stateAttributedStringForText(_ text: NSAttributedString) -> NSAttributedString { let sourceString = NSMutableAttributedString(attributedString: text) while true { @@ -45,6 +76,10 @@ public func stateAttributedStringForText(_ text: NSAttributedString) -> NSAttrib sourceString.replaceCharacters(in: range, with: NSAttributedString(string: value.text, attributes: [ChatTextInputAttributes.customEmoji: value.emoji])) stop.pointee = true found = true + } else if let value = value as? ChatInputTextCollapsedQuoteAttachment { + sourceString.replaceCharacters(in: range, with: value.text) + stop.pointee = true + found = true } }) if !found { @@ -102,7 +137,33 @@ public struct ChatTextFontAttributes: OptionSet, Hashable, Sequence { } } -public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) -> NSAttributedString { +public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, makeCollapsedQuoteAttachment: ((NSAttributedString, ChatInputTextCollapsedQuoteAttributes) -> ChatInputTextCollapsedQuoteAttachment)?) -> NSAttributedString { + let quoteAttributes = ChatInputTextCollapsedQuoteAttributes( + font: Font.regular(round(fontSize * 0.8235294117647058)), + textColor: textColor + ) + + let stateText = NSMutableAttributedString(attributedString: stateText) + + while true { + var found = false + stateText.enumerateAttribute(ChatTextInputAttributes.block, in: NSRange(location: 0, length: stateText.length), options: [.longestEffectiveRangeNotRequired], using: { value, range, stop in + if let value = value as? ChatTextInputTextQuoteAttribute { + if value.isCollapsed, let makeCollapsedQuoteAttachment { + found = true + stop.pointee = true + + let quoteText = stateText.attributedSubstring(from: range) + stateText.replaceCharacters(in: range, with: "") + stateText.insert(NSAttributedString(attachment: makeCollapsedQuoteAttachment(quoteText, quoteAttributes)), at: range.lowerBound) + } + } + }) + if !found { + break + } + } + let result = NSMutableAttributedString(string: stateText.string) let fullRange = NSRange(location: 0, length: result.length) @@ -157,6 +218,8 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo fontAttributes.insert(.monospace) } result.addAttribute(key, value: value, range: range) + } else if key == .attachment, value is ChatInputTextCollapsedQuoteAttachment { + result.addAttribute(key, value: value, range: range) } } @@ -656,11 +719,11 @@ private func refreshBlockQuotes(text: NSString, initialAttributedText: NSAttribu } } -public func refreshChatTextInputAttributes(_ textView: UITextView, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) { - refreshChatTextInputAttributes(textView: textView, primaryTextColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, baseFontSize: baseFontSize, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider) +public func refreshChatTextInputAttributes(_ textView: UITextView, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, makeCollapsedQuoteAttachment: ((NSAttributedString, ChatInputTextCollapsedQuoteAttributes) -> ChatInputTextCollapsedQuoteAttachment)?) { + refreshChatTextInputAttributes(textView: textView, primaryTextColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, baseFontSize: baseFontSize, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider, makeCollapsedQuoteAttachment: makeCollapsedQuoteAttachment) } -public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColor: UIColor, accentTextColor: UIColor, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) { +public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColor: UIColor, accentTextColor: UIColor, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, makeCollapsedQuoteAttachment: ((NSAttributedString, ChatInputTextCollapsedQuoteAttributes) -> ChatInputTextCollapsedQuoteAttachment)?) { guard let initialAttributedText = textView.attributedText, initialAttributedText.length != 0 else { return } @@ -677,21 +740,21 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo var attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(initialAttributedText)) refreshTextMentions(text: text, initialAttributedText: initialAttributedText, attributedText: attributedText, fullRange: fullRange) - var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider) + var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider, makeCollapsedQuoteAttachment: makeCollapsedQuoteAttachment) text = resultAttributedText.string as NSString fullRange = NSRange(location: 0, length: text.length) attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText)) refreshTextUrls(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange) - resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider) + resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider, makeCollapsedQuoteAttachment: makeCollapsedQuoteAttachment) text = resultAttributedText.string as NSString fullRange = NSRange(location: 0, length: text.length) attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText)) refreshBlockQuotes(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange) - resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider) + resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider, makeCollapsedQuoteAttachment: makeCollapsedQuoteAttachment) if !resultAttributedText.isEqual(to: initialAttributedText) { fullRange = NSRange(location: 0, length: textView.textStorage.length) @@ -750,13 +813,15 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo textView.textStorage.addAttribute(key, value: value, range: range) textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) } else if key == ChatTextInputAttributes.block, let value = value as? ChatTextInputTextQuoteAttribute { - switch value.kind { - case .quote: - fontAttributes.insert(.blockQuote) - case .code: - fontAttributes.insert(.monospace) + if !value.isCollapsed { + switch value.kind { + case .quote: + fontAttributes.insert(.blockQuote) + case .code: + fontAttributes.insert(.monospace) + } + textView.textStorage.addAttribute(key, value: value, range: range) } - textView.textStorage.addAttribute(key, value: value, range: range) } } @@ -799,7 +864,7 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo textView.textStorage.endEditing() } -public func refreshGenericTextInputAttributes(_ textView: UITextView, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, spoilersRevealed: Bool = false) { +public func refreshGenericTextInputAttributes(_ textView: UITextView, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, makeCollapsedQuoteAttachment: ((NSAttributedString, ChatInputTextCollapsedQuoteAttributes) -> ChatInputTextCollapsedQuoteAttachment)?, spoilersRevealed: Bool = false) { guard let initialAttributedText = textView.attributedText, initialAttributedText.length != 0 else { return } @@ -812,14 +877,14 @@ public func refreshGenericTextInputAttributes(_ textView: UITextView, theme: Pre var text: NSString = initialAttributedText.string as NSString var fullRange = NSRange(location: 0, length: initialAttributedText.length) var attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(initialAttributedText)) - var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider) + var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider, makeCollapsedQuoteAttachment: makeCollapsedQuoteAttachment) text = resultAttributedText.string as NSString fullRange = NSRange(location: 0, length: initialAttributedText.length) attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText)) refreshTextUrls(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange) - resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider) + resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider, makeCollapsedQuoteAttachment: makeCollapsedQuoteAttachment) if !resultAttributedText.isEqual(to: initialAttributedText) { textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange)