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) -> 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)? = nil, account: Account, text: String, link: String?, apply: @escaping (String?) -> Void) -> AlertController { +public func chatTextLinkEditController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = 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? + public let messageEffectId: Int64? - public init(replySubject: EngineMessageReplySubject?, text: String, entities: [MessageTextEntity], timestamp: Int32, textSelection: Range?) { + public init(replySubject: EngineMessageReplySubject?, text: String, entities: [MessageTextEntity], timestamp: Int32, textSelection: Range?, 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, .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) -> 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({