Story caption improvements

This commit is contained in:
Ilya Laktyushin
2023-06-20 18:05:20 +04:00
parent 3cb9b21c72
commit 09e2e5bdc2
56 changed files with 2903 additions and 666 deletions

View File

@@ -4,6 +4,17 @@ import Display
import ComponentFlow
import TextFormat
import TelegramPresentationData
import InvisibleInkDustNode
import EmojiTextAttachmentView
import AccountContext
import TextFormat
import ChatTextLinkEditUI
public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
public var enableInputClicksWhenVisible: Bool {
return true
}
}
public final class TextFieldComponent: Component {
public final class ExternalState {
@@ -27,18 +38,33 @@ public final class TextFieldComponent: Component {
}
}
public let context: AccountContext
public let strings: PresentationStrings
public let externalState: ExternalState
public let placeholder: String
public let fontSize: CGFloat
public let textColor: UIColor
public let insets: UIEdgeInsets
public let hideKeyboard: Bool
public let present: (ViewController) -> Void
public init(
context: AccountContext,
strings: PresentationStrings,
externalState: ExternalState,
placeholder: String
fontSize: CGFloat,
textColor: UIColor,
insets: UIEdgeInsets,
hideKeyboard: Bool,
present: @escaping (ViewController) -> Void
) {
self.context = context
self.strings = strings
self.externalState = externalState
self.placeholder = placeholder
self.fontSize = fontSize
self.textColor = textColor
self.insets = insets
self.hideKeyboard = hideKeyboard
self.present = present
}
public static func ==(lhs: TextFieldComponent, rhs: TextFieldComponent) -> Bool {
@@ -48,7 +74,16 @@ public final class TextFieldComponent: Component {
if lhs.externalState !== rhs.externalState {
return false
}
if lhs.placeholder != rhs.placeholder {
if lhs.fontSize != rhs.fontSize {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.hideKeyboard != rhs.hideKeyboard {
return false
}
return true
@@ -60,14 +95,21 @@ public final class TextFieldComponent: Component {
}
public final class View: UIView, UITextViewDelegate, UIScrollViewDelegate {
private let placeholder = ComponentView<Empty>()
private let textContainer: NSTextContainer
private let textStorage: NSTextStorage
private let layoutManager: NSLayoutManager
private let textView: UITextView
private var inputState = InputState(inputText: NSAttributedString(), selectionRange: 0 ..< 0)
private var spoilerView: InvisibleInkDustView?
private var customEmojiContainerView: CustomEmojiContainerView?
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
//private var inputState = InputState(inputText: NSAttributedString(), selectionRange: 0 ..< 0)
private var inputState: InputState {
let selectionRange: Range<Int> = self.textView.selectedRange.location ..< (self.textView.selectedRange.location + self.textView.selectedRange.length)
return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange)
}
private var component: TextFieldComponent?
private weak var state: EmptyComponentState?
@@ -88,7 +130,6 @@ public final class TextFieldComponent: Component {
self.textView = UITextView(frame: CGRect(), textContainer: self.textContainer)
self.textView.translatesAutoresizingMaskIntoConstraints = false
self.textView.textContainerInset = UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 8.0)
self.textView.backgroundColor = nil
self.textView.layer.isOpaque = false
self.textView.keyboardAppearance = .dark
@@ -115,58 +156,116 @@ public final class TextFieldComponent: Component {
fatalError("init(coder:) has not been implemented")
}
public func textViewDidChange(_ textView: UITextView) {
private func updateInputState(_ f: (InputState) -> InputState) {
guard let component = self.component else {
return
}
let inputState = f(self.inputState)
self.textView.attributedText = textAttributedStringForStateText(inputState.inputText, fontSize: component.fontSize, textColor: component.textColor, accentTextColor: component.textColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
self.textView.selectedRange = NSMakeRange(inputState.selectionRange.lowerBound, inputState.selectionRange.count)
refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.textColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
self.updateEntities()
}
public func insertText(_ text: NSAttributedString) {
self.updateInputState { state in
return state.insertText(text)
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
}
public func deleteBackward() {
self.textView.deleteBackward()
}
public func updateText(_ text: NSAttributedString, selectionRange: Range<Int>) {
self.updateInputState { _ in
return TextFieldComponent.InputState(inputText: text, selectionRange: selectionRange)
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
}
public func textViewDidChange(_ textView: UITextView) {
guard let component = self.component else {
return
}
refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.textColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
refreshChatTextInputTypingAttributes(self.textView, textColor: component.textColor, baseFontSize: component.fontSize)
self.updateEntities()
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
}
public func textViewDidChangeSelection(_ textView: UITextView) {
guard let _ = self.component else {
return
}
}
public func textViewDidBeginEditing(_ textView: UITextView) {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
}
public func textViewDidEndEditing(_ textView: UITextView) {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
}
@available(iOS 16.0, *)
public func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
let filteredActions: Set<String> = Set([
"com.apple.menu.format",
"com.apple.menu.replace"
])
let suggestedActions = suggestedActions.filter {
if let action = $0 as? UIMenu, filteredActions.contains(action.identifier.rawValue) {
return false
} else {
return true
}
}
guard let component = self.component, !textView.attributedText.string.isEmpty && textView.selectedRange.length > 0 else {
return UIMenu(children: suggestedActions)
}
let strings = component.strings
var actions: [UIAction] = [
UIAction(title: strings.TextFormat_Bold, image: nil) { [weak self] (action) in
UIAction(title: strings.TextFormat_Bold, image: nil) { [weak self] action in
if let self {
self.toggleAttribute(key: ChatTextInputAttributes.bold)
}
},
UIAction(title: strings.TextFormat_Italic, image: nil) { [weak self] (action) in
UIAction(title: strings.TextFormat_Italic, image: nil) { [weak self] action in
if let self {
self.toggleAttribute(key: ChatTextInputAttributes.italic)
}
},
UIAction(title: strings.TextFormat_Monospace, image: nil) { [weak self] (action) in
UIAction(title: strings.TextFormat_Monospace, image: nil) { [weak self] action in
if let self {
self.toggleAttribute(key: ChatTextInputAttributes.monospace)
}
},
UIAction(title: strings.TextFormat_Link, image: nil) { [weak self] (action) in
UIAction(title: strings.TextFormat_Link, image: nil) { [weak self] action in
if let self {
let _ = self
self.openLinkEditing()
}
},
UIAction(title: strings.TextFormat_Strikethrough, image: nil) { [weak self] (action) in
UIAction(title: strings.TextFormat_Strikethrough, image: nil) { [weak self] action in
if let self {
self.toggleAttribute(key: ChatTextInputAttributes.strikethrough)
}
},
UIAction(title: strings.TextFormat_Underline, image: nil) { [weak self] (action) in
UIAction(title: strings.TextFormat_Underline, image: nil) { [weak self] action in
if let self {
self.toggleAttribute(key: ChatTextInputAttributes.underline)
}
}
]
actions.append(UIAction(title: strings.TextFormat_Spoiler, image: nil) { [weak self] (action) in
actions.append(UIAction(title: strings.TextFormat_Spoiler, image: nil) { [weak self] action in
if let self {
self.toggleAttribute(key: ChatTextInputAttributes.spoiler)
}
@@ -174,27 +273,64 @@ public final class TextFieldComponent: Component {
var updatedActions = suggestedActions
let formatMenu = UIMenu(title: strings.TextFormat_Format, image: nil, children: actions)
updatedActions.insert(formatMenu, at: 3)
updatedActions.insert(formatMenu, at: 1)
return UIMenu(children: updatedActions)
}
private func toggleAttribute(key: NSAttributedString.Key) {
self.updateInputState { state in
return state.addFormattingAttribute(attribute: key)
}
}
private func openLinkEditing() {
guard let component = self.component else {
return
}
let selectionRange = self.inputState.selectionRange
let text = self.inputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count))
var link: String?
text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in
if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute {
link = linkAttribute.url
}
}
let controller = chatTextLinkEditController(sharedContext: component.context.sharedContext, account: component.context.account, text: text.string, link: link, apply: { [weak self] link in
if let self {
if let link = link {
self.updateInputState { state in
return state.addLinkAttribute(selectionRange: selectionRange, url: link)
}
}
// strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, {
// return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({
// $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex))
// })
// })
}
})
component.present(controller)
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
//print("didScroll \(scrollView.bounds)")
}
public func getInputState() -> TextFieldComponent.InputState {
return self.inputState
}
public func getAttributedText() -> NSAttributedString {
Keyboard.applyAutocorrection(textView: self.textView)
return NSAttributedString(string: self.textView.text ?? "")
//return self.inputState.inputText
return self.inputState.inputText
}
public func setAttributedText(_ string: NSAttributedString) {
self.textView.text = string.string
self.updateInputState { _ in
return TextFieldComponent.InputState(inputText: string, selectionRange: string.length ..< string.length)
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
}
@@ -202,90 +338,167 @@ public final class TextFieldComponent: Component {
self.textView.becomeFirstResponder()
}
var spoilersRevealed = false
func updateEntities() {
// var spoilerRects: [CGRect] = []
// var customEmojiRects: [CGRect: ChatTextInputTextCustomEmojiAttribute] = []
//
// if !spoilerRects.isEmpty {
// let dustNode: InvisibleInkDustNode
// if let current = self.dustNode {
// dustNode = current
// } else {
// dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true)
// dustNode.alpha = self.spoilersRevealed ? 0.0 : 1.0
// dustNode.isUserInteractionEnabled = false
// textInputNode.textView.addSubview(dustNode.view)
// self.dustNode = dustNode
// }
// dustNode.frame = CGRect(origin: CGPoint(), size: textInputNode.textView.contentSize)
// dustNode.update(size: textInputNode.textView.contentSize, color: textColor, textColor: textColor, rects: rects, wordRects: rects)
// } else if let dustNode = self.dustNode {
// dustNode.removeFromSupernode()
// self.dustNode = nil
// }
//
// if !customEmojiRects.isEmpty {
// let customEmojiContainerView: CustomEmojiContainerView
// if let current = self.customEmojiContainerView {
// customEmojiContainerView = current
// } else {
// customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in
// guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else {
// return nil
// }
// return emojiViewProvider(emoji)
// })
// customEmojiContainerView.isUserInteractionEnabled = false
// textInputNode.textView.addSubview(customEmojiContainerView)
// self.customEmojiContainerView = customEmojiContainerView
// }
//
// customEmojiContainerView.update(fontSize: fontSize, textColor: textColor, emojiRects: customEmojiRects)
// } else if let customEmojiContainerView = self.customEmojiContainerView {
// customEmojiContainerView.removeFromSuperview()
// self.customEmojiContainerView = nil
// }
guard let component = self.component else {
return
}
var spoilerRects: [CGRect] = []
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
let textView = self.textView
if let attributedText = textView.attributedText {
let beginning = textView.beginningOfDocument
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
if let _ = attributes[ChatTextInputAttributes.spoiler] {
func addSpoiler(startIndex: Int, endIndex: Int) {
if let start = textView.position(from: beginning, offset: startIndex), let end = textView.position(from: start, offset: endIndex - startIndex), let textRange = textView.textRange(from: start, to: end) {
let textRects = textView.selectionRects(for: textRange)
for textRect in textRects {
if textRect.rect.width > 1.0 && textRect.rect.size.height > 1.0 {
spoilerRects.append(textRect.rect.insetBy(dx: 1.0, dy: 1.0).offsetBy(dx: 0.0, dy: 1.0))
}
}
}
}
var startIndex: Int?
var currentIndex: Int?
let nsString = (attributedText.string as NSString)
nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in
if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil {
if let currentStartIndex = startIndex {
startIndex = nil
let endIndex = range.location
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
}
} else if startIndex == nil {
startIndex = range.location
}
currentIndex = range.location + range.length
}
if let currentStartIndex = startIndex, let currentIndex = currentIndex {
startIndex = nil
let endIndex = currentIndex
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
}
}
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) {
let textRects = textView.selectionRects(for: textRange)
for textRect in textRects {
customEmojiRects.append((textRect.rect, value))
break
}
}
}
})
}
if !spoilerRects.isEmpty {
let spoilerView: InvisibleInkDustView
if let current = self.spoilerView {
spoilerView = current
} else {
spoilerView = InvisibleInkDustView(textNode: nil, enableAnimations: component.context.sharedContext.energyUsageSettings.fullTranslucency)
spoilerView.alpha = self.spoilersRevealed ? 0.0 : 1.0
spoilerView.isUserInteractionEnabled = false
self.textView.addSubview(spoilerView)
self.spoilerView = spoilerView
}
spoilerView.frame = CGRect(origin: CGPoint(), size: self.textView.contentSize)
spoilerView.update(size: self.textView.contentSize, color: component.textColor, textColor: component.textColor, rects: spoilerRects, wordRects: spoilerRects)
} else if let spoilerView = self.spoilerView {
spoilerView.removeFromSuperview()
self.spoilerView = nil
}
if !customEmojiRects.isEmpty {
let customEmojiContainerView: CustomEmojiContainerView
if let current = self.customEmojiContainerView {
customEmojiContainerView = current
} else {
customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in
guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else {
return nil
}
return emojiViewProvider(emoji)
})
customEmojiContainerView.isUserInteractionEnabled = false
self.textView.addSubview(customEmojiContainerView)
self.customEmojiContainerView = customEmojiContainerView
}
customEmojiContainerView.update(fontSize: component.fontSize, textColor: component.textColor, emojiRects: customEmojiRects)
} else if let customEmojiContainerView = self.customEmojiContainerView {
customEmojiContainerView.removeFromSuperview()
self.customEmojiContainerView = nil
}
}
func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
if self.emojiViewProvider == nil {
self.emojiViewProvider = { [weak self] emoji in
guard let component = self?.component else {
return UIView()
}
let pointSize = floor(24.0 * 1.3)
return EmojiTextAttachmentView(context: component.context, userLocation: .other, emoji: emoji, file: emoji.file, cache: component.context.animationCache, renderer: component.context.animationRenderer, placeholderColor: UIColor.white.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize))
}
}
if self.textView.textContainerInset != component.insets {
self.textView.textContainerInset = component.insets
}
self.textContainer.size = CGSize(width: availableSize.width - self.textView.textContainerInset.left - self.textView.textContainerInset.right, height: 10000000.0)
self.layoutManager.ensureLayout(for: self.textContainer)
let boundingRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: self.textStorage.length), in: self.textContainer)
let size = CGSize(width: availableSize.width, height: min(200.0, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom))
let size = CGSize(width: availableSize.width, height: min(availableSize.height, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom))
let wasEditing = component.externalState.isEditing
let isEditing = self.textView.isFirstResponder
let refreshScrolling = self.textView.bounds.size != size
self.textView.frame = CGRect(origin: CGPoint(), size: size)
self.textView.panGestureRecognizer.isEnabled = isEditing
if refreshScrolling {
self.textView.setContentOffset(CGPoint(x: 0.0, y: max(0.0, self.textView.contentSize.height - self.textView.bounds.height)), animated: false)
}
let placeholderSize = self.placeholder.update(
transition: .immediate,
component: AnyComponent(Text(text: component.placeholder, font: Font.regular(17.0), color: UIColor(white: 1.0, alpha: 0.25))),
environment: {},
containerSize: availableSize
)
if let placeholderView = self.placeholder.view {
if placeholderView.superview == nil {
placeholderView.layer.anchorPoint = CGPoint()
placeholderView.isUserInteractionEnabled = false
self.insertSubview(placeholderView, belowSubview: self.textView)
if isEditing {
if wasEditing {
self.textView.setContentOffset(CGPoint(x: 0.0, y: max(0.0, self.textView.contentSize.height - self.textView.bounds.height)), animated: false)
}
} else {
self.textView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true)
}
let placeholderFrame = CGRect(origin: CGPoint(x: self.textView.textContainerInset.left + 5.0, y: self.textView.textContainerInset.top), size: placeholderSize)
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
placeholderView.isHidden = self.textStorage.length != 0
}
component.externalState.hasText = self.textStorage.length != 0
component.externalState.isEditing = self.textView.isFirstResponder
component.externalState.isEditing = isEditing
if component.hideKeyboard {
if self.textView.inputView == nil {
self.textView.inputView = EmptyInputView()
if self.textView.isFirstResponder {
self.textView.reloadInputViews()
}
}
} else {
if self.textView.inputView != nil {
self.textView.inputView = nil
if self.textView.isFirstResponder {
self.textView.reloadInputViews()
}
}
}
return size
}
@@ -299,3 +512,89 @@ public final class TextFieldComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
extension TextFieldComponent.InputState {
public func insertText(_ text: NSAttributedString) -> TextFieldComponent.InputState {
let inputText = NSMutableAttributedString(attributedString: self.inputText)
let range = self.selectionRange
inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: text)
let selectionPosition = range.lowerBound + (text.string as NSString).length
return TextFieldComponent.InputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition)
}
public func addFormattingAttribute(attribute: NSAttributedString.Key) -> TextFieldComponent.InputState {
if !self.selectionRange.isEmpty {
let nsRange = NSRange(location: self.selectionRange.lowerBound, length: self.selectionRange.count)
var addAttribute = true
var attributesToRemove: [NSAttributedString.Key] = []
self.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
for (key, _) in attributes {
if key == attribute && range == nsRange {
addAttribute = false
attributesToRemove.append(key)
}
}
}
let result = NSMutableAttributedString(attributedString: self.inputText)
for attribute in attributesToRemove {
result.removeAttribute(attribute, range: nsRange)
}
if addAttribute {
result.addAttribute(attribute, value: true as Bool, range: nsRange)
}
return TextFieldComponent.InputState(inputText: result, selectionRange: self.selectionRange)
} else {
return self
}
}
public func clearFormattingAttributes() -> TextFieldComponent.InputState {
if !self.selectionRange.isEmpty {
let nsRange = NSRange(location: self.selectionRange.lowerBound, length: self.selectionRange.count)
var attributesToRemove: [NSAttributedString.Key] = []
self.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
for (key, _) in attributes {
attributesToRemove.append(key)
}
}
let result = NSMutableAttributedString(attributedString: self.inputText)
for attribute in attributesToRemove {
result.removeAttribute(attribute, range: nsRange)
}
return TextFieldComponent.InputState(inputText: result, selectionRange: self.selectionRange)
} else {
return self
}
}
public func addLinkAttribute(selectionRange: Range<Int>, url: String) -> TextFieldComponent.InputState {
if !selectionRange.isEmpty {
let nsRange = NSRange(location: selectionRange.lowerBound, length: selectionRange.count)
var linkRange = nsRange
var attributesToRemove: [(NSAttributedString.Key, NSRange)] = []
self.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
for (key, _) in attributes {
if key == ChatTextInputAttributes.textUrl {
attributesToRemove.append((key, range))
linkRange = linkRange.union(range)
} else {
attributesToRemove.append((key, nsRange))
}
}
}
let result = NSMutableAttributedString(attributedString: self.inputText)
for (attribute, range) in attributesToRemove {
result.removeAttribute(attribute, range: range)
}
result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: nsRange)
return TextFieldComponent.InputState(inputText: result, selectionRange: selectionRange)
} else {
return self
}
}
}