diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 9603c2d28d..80b4d0ba55 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -590,6 +590,8 @@ extension StoreMessage { let isForumTopic = (innerFlags & (1 << 3)) != 0 var quote: EngineMessageReplyQuote? + let isQuote = (innerFlags & (1 << 9)) != 0 + if quoteText != nil || replyMedia != nil { quote = EngineMessageReplyQuote(text: quoteText ?? "", entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []), media: textMediaAndExpirationTimerFromApiMedia(replyMedia, peerId).media) } @@ -626,7 +628,7 @@ extension StoreMessage { attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote)) } if let replyHeader = replyHeader { - attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote)) + attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote, isQuote: isQuote)) } case let .messageReplyStoryHeader(userId, storyId): attributes.append(ReplyStoryAttribute(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: storyId))) @@ -867,8 +869,9 @@ extension StoreMessage { if let replyTo = replyTo { var threadMessageId: MessageId? switch replyTo { - case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities): + case let .messageReplyHeader(innerFlags, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities): var quote: EngineMessageReplyQuote? + let isQuote = (innerFlags & (1 << 9)) != 0 if quoteText != nil || replyMedia != nil { quote = EngineMessageReplyQuote(text: quoteText ?? "", entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []), media: textMediaAndExpirationTimerFromApiMedia(replyMedia, peerId).media) } @@ -894,7 +897,7 @@ extension StoreMessage { } attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote)) } else if let replyHeader = replyHeader { - attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote)) + attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote, isQuote: isQuote)) } case let .messageReplyStoryHeader(userId, storyId): attributes.append(ReplyStoryAttribute(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: storyId))) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift index 5f7bcb3c03..ba5376fd90 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift @@ -51,6 +51,7 @@ public class QuotedReplyMessageAttribute: MessageAttribute { public let peerId: PeerId? public let authorName: String? public let quote: EngineMessageReplyQuote? + public let isQuote: Bool public var associatedMessageIds: [MessageId] { return [] @@ -64,16 +65,18 @@ public class QuotedReplyMessageAttribute: MessageAttribute { } } - public init(peerId: PeerId?, authorName: String?, quote: EngineMessageReplyQuote?) { + public init(peerId: PeerId?, authorName: String?, quote: EngineMessageReplyQuote?, isQuote: Bool) { self.peerId = peerId self.authorName = authorName self.quote = quote + self.isQuote = isQuote } required public init(decoder: PostboxDecoder) { self.peerId = decoder.decodeOptionalInt64ForKey("p").flatMap(PeerId.init) self.authorName = decoder.decodeOptionalStringForKey("a") self.quote = decoder.decodeCodable(EngineMessageReplyQuote.self, forKey: "qu") + self.isQuote = decoder.decodeBoolForKey("iq", orElse: true) } public func encode(_ encoder: PostboxEncoder) { @@ -94,14 +97,16 @@ public class QuotedReplyMessageAttribute: MessageAttribute { } else { encoder.encodeNil(forKey: "qu") } + + encoder.encodeBool(self.isQuote, forKey: "iq") } } extension QuotedReplyMessageAttribute { - convenience init(apiHeader: Api.MessageFwdHeader, quote: EngineMessageReplyQuote?) { + convenience init(apiHeader: Api.MessageFwdHeader, quote: EngineMessageReplyQuote?, isQuote: Bool) { switch apiHeader { case let .messageFwdHeader(_, fromId, fromName, _, _, _, _, _, _): - self.init(peerId: fromId?.peerId, authorName: fromName, quote: quote) + self.init(peerId: fromId?.peerId, authorName: fromName, quote: quote, isQuote: isQuote) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h index fc04921265..528c45c33f 100755 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h @@ -19,6 +19,7 @@ @property (nonatomic, copy) bool (^ _Nullable shouldRespondToAction)(SEL _Nullable); @property (nonatomic, copy) bool (^ _Nullable shouldReturn)(); @property (nonatomic, copy) void (^ _Nullable backspaceWhileEmpty)(); +@property (nonatomic, copy) void (^ _Nullable dropAutocorrectioniOS16)(); - (instancetype _Nonnull)initWithFrame:(CGRect)frame textContainer:(NSTextContainer * _Nullable)textContainer disableTiling:(bool)disableTiling; diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m index a92c35160a..8e96504a12 100755 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m @@ -12,6 +12,12 @@ @end +@interface ChatInputTextViewImpl () { + UIGestureRecognizer *_tapRecognizer; +} + +@end + @implementation ChatInputTextViewImpl - (instancetype _Nonnull)initWithFrame:(CGRect)frame textContainer:(NSTextContainer * _Nullable)textContainer disableTiling:(bool)disableTiling { @@ -26,10 +32,40 @@ #pragma clang diagnostic pop } } + + if (@available(iOS 17.0, *)) { + } else { + _tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(workaroundTapGesture:)]; + _tapRecognizer.cancelsTouchesInView = false; + _tapRecognizer.delaysTouchesBegan = false; + _tapRecognizer.delaysTouchesEnded = false; + _tapRecognizer.delegate = self; + [self addGestureRecognizer:_tapRecognizer]; + } } return self; } +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + return true; +} + +- (void)workaroundTapGesture:(UITapGestureRecognizer *)recognizer { + if (recognizer.state == UIGestureRecognizerStateEnded) { + static Class promptClass = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + promptClass = NSClassFromString([[NSString alloc] initWithFormat:@"%@AutocorrectInlinePrompt", @"UI"]); + }); + UIView *result = [self hitTest:[recognizer locationInView:self] withEvent:nil]; + if (result != nil && [result class] == promptClass) { + if (_dropAutocorrectioniOS16) { + _dropAutocorrectioniOS16(); + } + } + } +} + - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { if (_shouldRespondToAction) { diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift index 5ad648a733..57bd4d64bb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift @@ -32,7 +32,6 @@ open class ChatInputTextNode: ASDisplayNode, UITextViewDelegate { } private var selectionChangedForEditedText: Bool = false - private var isPreservingSelection: Bool = false public var textView: ChatInputTextView { return self.view as! ChatInputTextView @@ -81,20 +80,7 @@ open class ChatInputTextNode: ASDisplayNode, UITextViewDelegate { get { return self.textView.attributedText } set(value) { - if self.textView.attributedText != value { - let selectedRange = self.textView.selectedRange; - let preserveSelectedRange = selectedRange.location != self.textView.textStorage.length - - self.textView.attributedText = value ?? NSAttributedString() - - if preserveSelectedRange { - self.isPreservingSelection = true - self.textView.selectedRange = selectedRange - self.isPreservingSelection = false - } - - self.textView.updateTextContainerInset() - } + self.textView.attributedText = value } } @@ -164,7 +150,7 @@ open class ChatInputTextNode: ASDisplayNode, UITextViewDelegate { } @objc public func textViewDidChangeSelection(_ textView: UITextView) { - if self.isPreservingSelection { + if self.textView.isPreservingSelection { return } @@ -187,6 +173,9 @@ open class ChatInputTextNode: ASDisplayNode, UITextViewDelegate { guard let delegate = self.delegate else { return true } + if self.textView.isPreservingText { + return false + } return delegate.chatInputTextNode(shouldChangeTextIn: range, replacementText: text) } @@ -306,6 +295,30 @@ public final class ChatInputTextView: ChatInputTextViewImpl, NSLayoutManagerDele } } + override public var attributedText: NSAttributedString? { + get { + return super.attributedText + } set(value) { + if self.attributedText != value { + let selectedRange = self.selectedRange + let preserveSelectedRange = selectedRange.location != self.textStorage.length + + super.attributedText = value ?? NSAttributedString() + + if preserveSelectedRange { + self.isPreservingSelection = true + self.selectedRange = selectedRange + self.isPreservingSelection = false + } + + self.updateTextContainerInset() + } + } + } + + fileprivate var isPreservingSelection: Bool = false + fileprivate var isPreservingText: Bool = false + public weak var customDelegate: ChatInputTextNodeDelegate? public var theme: Theme? { @@ -391,6 +404,27 @@ public final class ChatInputTextView: ChatInputTextViewImpl, NSLayoutManagerDele self.customTextStorage.delegate = self self.measurementTextStorage.delegate = self + self.dropAutocorrectioniOS16 = { [weak self] in + guard let self else { + return + } + + self.isPreservingSelection = true + self.isPreservingText = true + + let rangeCopy = self.selectedRange + var fakeRange = rangeCopy + if fakeRange.location != 0 { + fakeRange.location -= 1 + } + self.unmarkText() + self.selectedRange = fakeRange + self.selectedRange = rangeCopy + + self.isPreservingSelection = false + self.isPreservingText = false + } + self.shouldCopy = { [weak self] in guard let self else { return true @@ -542,7 +576,7 @@ public final class ChatInputTextView: ChatInputTextViewImpl, NSLayoutManagerDele if self.measurementTextStorage != self.attributedText || self.measurementTextContainer.size != measureSize || self.measurementTextContainer.rightInset != rightInset { self.measurementTextContainer.rightInset = rightInset - self.measurementTextStorage.setAttributedString(self.attributedText) + self.measurementTextStorage.setAttributedString(self.attributedText ?? NSAttributedString()) self.measurementTextContainer.size = measureSize self.measurementLayoutManager.invalidateLayout(forCharacterRange: NSRange(location: 0, length: self.measurementTextStorage.length), actualCharacterRange: nil) self.measurementLayoutManager.ensureLayout(for: self.measurementTextContainer) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index 1645b42d4d..7413ee54dd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -547,7 +547,15 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { var adjustedConstrainedTextSize = contrainedTextSize var textCutout: TextNodeCutout? var textCutoutWidth: CGFloat = 0.0 - if arguments.quote != nil || arguments.replyForward?.quote != nil { + + var isQuote = false + if arguments.quote != nil { + isQuote = true + } else if let replyForward = arguments.replyForward, replyForward.quote != nil, replyForward.isQuote { + isQuote = true + } + + if isQuote { additionalTitleWidth += 10.0 maxTitleNumberOfLines = 2 maxTextNumberOfLines = 5 @@ -770,7 +778,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { animation: animation ) - if arguments.quote != nil || arguments.replyForward?.quote != nil { + if isQuote { let quoteIconView: UIImageView if let current = node.quoteIconView { quoteIconView = current diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 2ce05e2c0d..7daebc0a99 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -2849,8 +2849,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch var hasTracking = false var hasTrackingView = false - if textInputNode.selectedRange.length == 0 && textInputNode.selectedRange.location > 0 { - let selectedSubstring = textInputNode.textView.attributedText.attributedSubstring(from: NSRange(location: 0, length: textInputNode.selectedRange.location)) + if textInputNode.selectedRange.length == 0, textInputNode.selectedRange.location > 0, let attributedText = textInputNode.textView.attributedText { + let selectedSubstring = attributedText.attributedSubstring(from: NSRange(location: 0, length: textInputNode.selectedRange.location)) if let lastCharacter = selectedSubstring.string.last, String(lastCharacter).isSingleEmoji { let queryLength = (String(lastCharacter) as NSString).length if selectedSubstring.attribute(ChatTextInputAttributes.customEmoji, at: selectedSubstring.length - queryLength, effectiveRange: nil) == nil {