From dcfc4d9364ecefee748066f34ec4edcd47f27072 Mon Sep 17 00:00:00 2001
From: Isaac <>
Date: Fri, 7 Jun 2024 19:34:28 +0400
Subject: [PATCH] Input state updates

---
 .../Sources/ChatInterfaceState.swift          |  2 +-
 .../Sources/ChatTextFormat.swift              | 24 ++++++++++++
 .../Sources/ChatTextLinkEditController.swift  | 20 ++++++++--
 .../State/AccountStateManagementUtils.swift   |  4 +-
 ...ncCore_SynchronizeableChatInputState.swift |  9 ++++-
 .../Sources/TextFieldComponent.swift          | 38 +++++++++++++++++--
 .../Chat/ChatControllerLoadDisplayNode.swift  | 16 +++++---
 7 files changed, 95 insertions(+), 18 deletions(-)

diff --git a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift
index 1063ec0211..bf5f3dfe82 100644
--- a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift
+++ b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift
@@ -478,7 +478,7 @@ public final class ChatInterfaceState: Codable, Equatable {
             return nil
         } else {
             let sourceText = expandedInputStateAttributedString(self.composeInputState.inputText)
-            return SynchronizeableChatInputState(replySubject: self.replyMessageSubject?.subjectModel, text: sourceText.string, entities: generateChatInputTextEntities(sourceText), timestamp: self.timestamp, textSelection: self.composeInputState.selectionRange)
+            return SynchronizeableChatInputState(replySubject: self.replyMessageSubject?.subjectModel, text: sourceText.string, entities: generateChatInputTextEntities(sourceText), timestamp: self.timestamp, textSelection: self.composeInputState.selectionRange, messageEffectId: self.sendMessageEffect)
         }
     }
 
diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift
index 6c76dab4ed..a0f301fccf 100644
--- a/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift
+++ b/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift
@@ -145,6 +145,30 @@ public func chatTextInputAddLinkAttribute(_ state: ChatTextInputState, selection
     }
 }
 
+public func chatTextInputRemoveLinkAttribute(_ state: ChatTextInputState, selectionRange: Range<Int>) -> ChatTextInputState {
+    if !selectionRange.isEmpty {
+        let nsRange = NSRange(location: selectionRange.lowerBound, length: selectionRange.count)
+        var attributesToRemove: [(NSAttributedString.Key, NSRange)] = []
+        state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
+            for (key, _) in attributes {
+                if key == ChatTextInputAttributes.textUrl {
+                    attributesToRemove.append((key, range))
+                } else {
+                    attributesToRemove.append((key, nsRange))
+                }
+            }
+        }
+        
+        let result = NSMutableAttributedString(attributedString: state.inputText)
+        for (attribute, range) in attributesToRemove {
+            result.removeAttribute(attribute, range: range)
+        }
+        return ChatTextInputState(inputText: result, selectionRange: selectionRange)
+    } else {
+        return state
+    }
+}
+
 public func chatTextInputAddMentionAttribute(_ state: ChatTextInputState, peer: EnginePeer) -> ChatTextInputState {
     let inputText = NSMutableAttributedString(attributedString: state.inputText)
     
diff --git a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift
index 85e4268977..8ed99a1fb8 100644
--- a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift
+++ b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift
@@ -176,11 +176,13 @@ private final class ChatTextLinkEditAlertContentNode: AlertContentNode {
     }
     
     private var isEditing = false
+    private let allowEmpty: Bool
     
-    init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, link: String?) {
+    init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, link: String?, allowEmpty: Bool) {
         self.strings = strings
         self.text = text
         self.isEditing = link != nil
+        self.allowEmpty = allowEmpty
         
         self.titleNode = ASTextNode()
         self.titleNode.maximumNumberOfLines = 2
@@ -220,6 +222,9 @@ private final class ChatTextLinkEditAlertContentNode: AlertContentNode {
             self.addSubnode(actionNode)
         }
         self.actionNodes.last?.actionEnabled = !(link ?? "").isEmpty
+        if allowEmpty {
+            self.actionNodes.last?.actionEnabled = true
+        }
         
         for separatorNode in self.actionVerticalSeparators {
             self.addSubnode(separatorNode)
@@ -235,7 +240,11 @@ private final class ChatTextLinkEditAlertContentNode: AlertContentNode {
         
         self.inputFieldNode.textChanged = { [weak self] text in
             if let strongSelf = self, let lastNode = strongSelf.actionNodes.last {
-                lastNode.actionEnabled = !text.isEmpty
+                if strongSelf.allowEmpty {
+                    lastNode.actionEnabled = true
+                } else {
+                    lastNode.actionEnabled = !text.isEmpty
+                }
             }
         }
         
@@ -402,7 +411,7 @@ private final class ChatTextLinkEditAlertContentNode: AlertContentNode {
     }
 }
 
-public func chatTextLinkEditController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, account: Account, text: String, link: String?, apply: @escaping (String?) -> Void) -> AlertController {
+public func chatTextLinkEditController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, account: Account, text: String, link: String?, allowEmpty: Bool = false, apply: @escaping (String?) -> Void) -> AlertController {
     let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 }
     
     var dismissImpl: ((Bool) -> Void)?
@@ -415,7 +424,7 @@ public func chatTextLinkEditController(sharedContext: SharedAccountContext, upda
         applyImpl?()
     })]
     
-    let contentNode = ChatTextLinkEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, link: link)
+    let contentNode = ChatTextLinkEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, link: link, allowEmpty: allowEmpty)
     contentNode.complete = {
         applyImpl?()
     }
@@ -427,6 +436,9 @@ public func chatTextLinkEditController(sharedContext: SharedAccountContext, upda
         if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false]) {
             dismissImpl?(true)
             apply(updatedLink)
+        } else if allowEmpty && contentNode.link.isEmpty {
+            dismissImpl?(true)
+            apply("")
         } else {
             contentNode.animateError()
         }
diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift
index 68fd63150b..da19c7176e 100644
--- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift
+++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift
@@ -1554,7 +1554,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
                 switch draft {
                     case .draftMessageEmpty:
                         inputState = nil
-                    case let .draftMessage(_, replyToMsgHeader, message, entities, media, date, _):
+                    case let .draftMessage(_, replyToMsgHeader, message, entities, media, date, messageEffectId):
                         let _ = media
                         var replySubject: EngineMessageReplySubject?
                         if let replyToMsgHeader = replyToMsgHeader {
@@ -1600,7 +1600,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
                                 break
                             }
                         }
-                        inputState = SynchronizeableChatInputState(replySubject: replySubject, text: message, entities: messageTextEntitiesFromApiEntities(entities ?? []), timestamp: date, textSelection: nil)
+                        inputState = SynchronizeableChatInputState(replySubject: replySubject, text: message, entities: messageTextEntitiesFromApiEntities(entities ?? []), timestamp: date, textSelection: nil, messageEffectId: messageEffectId)
                 }
                 var threadId: Int64?
                 if let topMsgId = topMsgId {
diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift
index c802fb9a4c..32d49fc4fd 100644
--- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift
+++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift
@@ -7,13 +7,15 @@ public struct SynchronizeableChatInputState: Codable, Equatable {
     public let entities: [MessageTextEntity]
     public let timestamp: Int32
     public let textSelection: Range<Int>?
+    public let messageEffectId: Int64?
     
-    public init(replySubject: EngineMessageReplySubject?, text: String, entities: [MessageTextEntity], timestamp: Int32, textSelection: Range<Int>?) {
+    public init(replySubject: EngineMessageReplySubject?, text: String, entities: [MessageTextEntity], timestamp: Int32, textSelection: Range<Int>?, messageEffectId: Int64?) {
         self.replySubject = replySubject
         self.text = text
         self.entities = entities
         self.timestamp = timestamp
         self.textSelection = textSelection
+        self.messageEffectId = messageEffectId
     }
     
     public init(from decoder: Decoder) throws {
@@ -32,6 +34,7 @@ public struct SynchronizeableChatInputState: Codable, Equatable {
             }
         }
         self.textSelection = nil
+        self.messageEffectId = try container.decodeIfPresent(Int64.self, forKey: "messageEffectId")
     }
     
     public func encode(to encoder: Encoder) throws {
@@ -41,6 +44,7 @@ public struct SynchronizeableChatInputState: Codable, Equatable {
         try container.encode(self.entities, forKey: "e")
         try container.encode(self.timestamp, forKey: "s")
         try container.encodeIfPresent(self.replySubject, forKey: "rep")
+        try container.encodeIfPresent(self.messageEffectId, forKey: "messageEffectId")
     }
     
     public static func ==(lhs: SynchronizeableChatInputState, rhs: SynchronizeableChatInputState) -> Bool {
@@ -59,6 +63,9 @@ public struct SynchronizeableChatInputState: Codable, Equatable {
         if lhs.textSelection != rhs.textSelection {
             return false
         }
+        if lhs.messageEffectId != rhs.messageEffectId {
+            return false
+        }
         return true
     }
 }
diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift
index 0d31238442..3a9b6894a8 100644
--- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift
+++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift
@@ -930,11 +930,17 @@ public final class TextFieldComponent: Component {
             
             let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: component.theme)
             let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) = (presentationData, .single(presentationData))
-            let controller = chatTextLinkEditController(sharedContext: component.context.sharedContext, updatedPresentationData: updatedPresentationData, account: component.context.account, text: text.string, link: link, apply: { [weak self] link in
+            let controller = chatTextLinkEditController(sharedContext: component.context.sharedContext, updatedPresentationData: updatedPresentationData, account: component.context.account, text: text.string, link: link, allowEmpty: true, apply: { [weak self] link in
                 if let self {
-                    if let link = link {
-                        self.updateInputState { state in
-                            return state.addLinkAttribute(selectionRange: selectionRange, url: link)
+                    if let link {
+                        if !link.isEmpty {
+                            self.updateInputState { state in
+                                return state.addLinkAttribute(selectionRange: selectionRange, url: link)
+                            }
+                        } else {
+                            self.updateInputState { state in
+                                return state.removeLinkAttribute(selectionRange: selectionRange)
+                            }
                         }
                         self.textView.becomeFirstResponder()
                     }
@@ -1585,4 +1591,28 @@ extension TextFieldComponent.InputState {
             return self
         }
     }
+    
+    public func removeLinkAttribute(selectionRange: Range<Int>) -> TextFieldComponent.InputState {
+        if !selectionRange.isEmpty {
+            let nsRange = NSRange(location: selectionRange.lowerBound, length: selectionRange.count)
+            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))
+                    } else {
+                        attributesToRemove.append((key, nsRange))
+                    }
+                }
+            }
+
+            let result = NSMutableAttributedString(attributedString: self.inputText)
+            for (attribute, range) in attributesToRemove {
+                result.removeAttribute(attribute, range: range)
+            }
+            return TextFieldComponent.InputState(inputText: result, selectionRange: selectionRange)
+        } else {
+            return self
+        }
+    }
 }
diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift
index d68c4f8bf0..d8951af158 100644
--- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift
+++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift
@@ -3706,14 +3706,18 @@ extension ChatControllerImpl {
                     }
                 }
                 
-                let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: strongSelf.updatedPresentationData, account: strongSelf.context.account, text: text?.string ?? "", link: link, apply: { [weak self] link in
+                let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: strongSelf.updatedPresentationData, account: strongSelf.context.account, text: text?.string ?? "", link: link, allowEmpty: true, apply: { [weak self] link in
                     if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange {
-                        if let link = link {
-                            strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
-                                return (chatTextInputAddLinkAttribute(current, selectionRange: selectionRange, url: link), inputMode)
+                        if let link {
+                            if !link.isEmpty {
+                                strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
+                                    return (chatTextInputAddLinkAttribute(current, selectionRange: selectionRange, url: link), inputMode)
+                                }
+                            } else {
+                                strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
+                                    return (chatTextInputRemoveLinkAttribute(current, selectionRange: selectionRange), inputMode)
+                                }
                             }
-                        } else {
-                            
                         }
                         strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, {
                             return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({