From eae866c77e6a8071ee5ee831445dba6e69e38784 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 7 Oct 2023 00:33:12 +0400 Subject: [PATCH] [WIP] Quotes --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Source/ASEditableTextNode.mm | 72 +- .../Source/ASTextKitComponents.mm | 35 +- submodules/AttachmentTextInputPanelNode/BUILD | 1 + .../AttachmentTextInputPanelNode.swift | 105 ++- .../Sources/AttachmentPanel.swift | 2 +- .../Sources/ChatInterfaceState.swift | 99 ++- .../Sources/ChatTextFormat.swift | 12 +- submodules/ChatSendMessageActionUI/BUILD | 1 + ...ChatSendMessageActionSheetController.swift | 8 +- ...SendMessageActionSheetControllerNode.swift | 57 +- .../Sources/CreatePollTextInputItem.swift | 14 +- .../Display/Source/EditableTextNode.swift | 16 + submodules/Display/Source/TextNode.swift | 13 +- .../ChatItemGalleryFooterContentNode.swift | 2 + .../GalleryUI/Sources/GalleryController.swift | 2 +- .../BubbleSettingsController.swift | 2 +- .../TextSizeSelectionController.swift | 2 +- .../ThemeAccentColorControllerNode.swift | 4 +- .../Themes/ThemePreviewControllerNode.swift | 4 +- .../Themes/ThemeSettingsChatPreviewItem.swift | 2 +- .../Sources/ShareController.swift | 26 +- .../ApiUtils/StoreMessage_Telegram.swift | 18 +- .../PendingMessages/EnqueueMessage.swift | 50 +- .../StandaloneSendMessage.swift | 2 +- .../State/AccountStateManagementUtils.swift | 23 +- ...dSynchronizeChatInputStateOperations.swift | 54 +- .../Sources/State/PendingMessageManager.swift | 104 ++- ...ecretChatIncomingDecryptedOperations.swift | 8 +- .../SyncCore_ReplyMessageAttribute.swift | 11 +- ...ncCore_SynchronizeableChatInputState.swift | 30 +- ...OutgoingMessageWithChatContextResult.swift | 6 +- .../Messages/TelegramEngineMessages.swift | 6 +- .../Peers/TelegramEnginePeers.swift | 2 +- .../Resources/PresentationResourceKey.swift | 2 + .../Resources/PresentationResourcesChat.swift | 19 + submodules/TelegramUI/BUILD | 1 + .../Components/Chat/ChatInputTextNode/BUILD | 21 + .../ChatInputTextViewImpl/BUILD | 23 + .../ChatInputTextViewImpl.h | 25 + .../Sources/ChatInputTextViewImpl.m | 100 +++ .../Sources/ChatInputTextNode.swift | 691 ++++++++++++++++++ .../Sources/ChatMessageItemCommon.swift | 8 +- .../Chat/ChatMessageReplyInfoNode/BUILD | 20 + .../Sources/AccessoryPanelNode.swift | 23 + .../ChatMessageTextBubbleContentNode.swift | 5 +- .../Sources/ChatControllerInteraction.swift | 4 +- .../Sources/PeerSelectionControllerNode.swift | 2 +- .../StoryContentCaptionComponent.swift | 3 +- .../StoryItemSetContainerComponent.swift | 2 + ...StoryItemSetContainerViewSendMessage.swift | 8 +- .../TelegramUI/Sources/AppDelegate.swift | 2 +- .../TelegramUI/Sources/ChatBotInfoItem.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 251 ++++--- .../Sources/ChatControllerNode.swift | 12 +- .../ChatInterfaceStateAccessoryPanels.swift | 6 +- .../ChatInterfaceStateContextMenus.swift | 4 +- .../ChatInterfaceStateInputPanels.swift | 4 +- .../ChatMessageAnimatedStickerItemNode.swift | 12 +- .../ChatMessageAttachedContentNode.swift | 2 +- .../Sources/ChatMessageBubbleItemNode.swift | 24 +- .../ChatMessageInstantVideoItemNode.swift | 10 +- .../ChatMessageInteractiveFileNode.swift | 3 +- ...atMessageInteractiveInstantVideoNode.swift | 10 +- .../Sources/ChatMessageItemView.swift | 4 +- .../Sources/ChatMessageReplyInfoNode.swift | 96 ++- .../Sources/ChatMessageStickerItemNode.swift | 10 +- .../ChatRecentActionsControllerNode.swift | 2 +- .../Sources/ChatTextInputPanelNode.swift | 139 +++- .../OverlayAudioPlayerControllerNode.swift | 2 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 2 +- .../Sources/ReplyAccessoryPanelNode.swift | 16 +- .../Sources/SharedAccountContext.swift | 2 +- .../Sources/ChatTextInputAttributes.swift | 259 ++++--- .../Sources/StringWithAppliedEntities.swift | 14 +- .../Sources/TextSelectionNode.swift | 11 +- .../Sources/WatchRequestHandlers.swift | 4 +- 77 files changed, 2103 insertions(+), 552 deletions(-) create mode 100644 submodules/TelegramUI/Components/Chat/ChatInputTextNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/BUILD create mode 100755 submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h create mode 100755 submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m create mode 100644 submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/AccessoryPanelNode.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index b6d52aa300..a4b699fbc9 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10095,3 +10095,5 @@ Sorry for the inconvenience."; "PremiumGift.LabelRecipients_1" = "1 recipient"; "PremiumGift.LabelRecipients_any" = "%d recipients"; + +"Conversation.ContextMenuQuote" = "Quote"; diff --git a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm index ba05b9d895..5ddf0da07a 100644 --- a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm +++ b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm @@ -244,6 +244,11 @@ [super scrollRectToVisible:rect animated:false]; } +- (CGRect)caretRectForPosition:(UITextPosition *)position { + CGRect rect = [super caretRectForPosition:position]; + return rect; +} + #endif - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer @@ -501,6 +506,9 @@ - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset { AS::MutexLocker l(_textKitLock); + + textContainerInset.top += 12.0; + textContainerInset.bottom += 12.0; _textContainerInset = textContainerInset; _textKitComponents.textView.textContainerInset = textContainerInset; @@ -1062,8 +1070,68 @@ return [_wordKerner layoutManager:layoutManager boundingBoxForControlGlyphAtIndex:glyphIndex forTextContainer:textContainer proposedLineFragment:proposedRect glyphPosition:glyphPosition characterIndex:characterIndex]; } +- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager paragraphSpacingBeforeGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect { + int characterIndex = (int)[layoutManager characterIndexForGlyphAtIndex:glyphIndex]; + if (characterIndex < 0 || characterIndex >= layoutManager.textStorage.length) { + return 0.0; + } + + NSDictionary *attributes = [layoutManager.textStorage attributesAtIndex:characterIndex effectiveRange:nil]; + NSObject *blockQuote = attributes[@"Attribute__Blockquote"]; + if (blockQuote == nil) { + return 0.0f; + } + + if (characterIndex != 0) { + NSDictionary *previousAttributes = [layoutManager.textStorage attributesAtIndex:characterIndex - 1 effectiveRange:nil]; + NSObject *previousBlockQuote = previousAttributes[@"Attribute__Blockquote"]; + if (previousBlockQuote != nil && [blockQuote isEqual:previousBlockQuote]) { + return 0.0f; + } + } + + return 12.0f; +} + +- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager paragraphSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect { + int characterIndex = (int)[layoutManager characterIndexForGlyphAtIndex:glyphIndex]; + characterIndex--; + if (characterIndex < 0) { + characterIndex = 0; + } + if (characterIndex < 0 || characterIndex >= layoutManager.textStorage.length) { + return 0.0; + } + + NSDictionary *attributes = [layoutManager.textStorage attributesAtIndex:characterIndex effectiveRange:nil]; + NSObject *blockQuote = attributes[@"Attribute__Blockquote"]; + if (blockQuote == nil) { + return 0.0f; + } + + if (characterIndex + 1 < layoutManager.textStorage.length) { + NSDictionary *nextAttributes = [layoutManager.textStorage attributesAtIndex:characterIndex + 1 effectiveRange:nil]; + NSObject *nextBlockQuote = nextAttributes[@"Attribute__Blockquote"]; + if (nextBlockQuote != nil && [blockQuote isEqual:nextBlockQuote]) { + return 0.0f; + } + } + + return 12.0f; +} + - (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldSetLineFragmentRect:(inout CGRect *)lineFragmentRect lineFragmentUsedRect:(inout CGRect *)lineFragmentUsedRect baselineOffset:(inout CGFloat *)baselineOffset inTextContainer:(NSTextContainer *)textContainer forGlyphRange:(NSRange)glyphRange { - CGFloat fontLineHeight; + /*if (layoutManager.textStorage.length != 0) { + NSDictionary *attributes = [layoutManager.textStorage attributesAtIndex:0 effectiveRange:nil]; + NSObject *blockQuote = attributes[@"Attribute__Blockquote"]; + if (blockQuote != nil) { + CGRect rect = *lineFragmentRect; + rect.origin.y += 12.0; + CGRect usedRect = *lineFragmentUsedRect; + usedRect.origin.y += 12.0; + } + }*/ + /*CGFloat fontLineHeight; UIFont *baseFont = _baseFont; if (_typingAttributes[NSFontAttributeName] != nil) { baseFont = _typingAttributes[NSFontAttributeName]; @@ -1086,7 +1154,7 @@ *lineFragmentRect = rect; *lineFragmentUsedRect = usedRect; - *baselineOffset = *baselineOffset + baselineNudge; + *baselineOffset = *baselineOffset + baselineNudge;*/ return true; } diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm index be98e25708..1b8754c8ee 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm +++ b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm @@ -23,17 +23,40 @@ return self; } +- (BOOL)isSimpleRectangularTextContainer { + return false; +} + - (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(nullable CGRect *)remainingRect { CGRect result = [super lineFragmentRectForProposedRect:proposedRect atIndex:characterIndex writingDirection:baseWritingDirection remainingRect:remainingRect]; -/*#if DEBUG - if (result.origin.y < 10.0f) { - result.size.width -= 21.0f; - if (result.size.width < 0.0f) { - result.size.width = 0.0f; + NSTextStorage *textStorage = self.layoutManager.textStorage; + if (textStorage != nil) { + NSString *string = textStorage.string; + int index = (int)characterIndex; + if (index >= 0 && index < string.length) { + NSDictionary *attributes = [textStorage attributesAtIndex:index effectiveRange:nil]; + NSObject *blockQuote = attributes[@"Attribute__Blockquote"]; + if (blockQuote != nil) { + bool isFirstLine = false; + if (index == 0) { + isFirstLine = true; + } else { + NSDictionary *previousAttributes = [textStorage attributesAtIndex:index - 1 effectiveRange:nil]; + NSObject *previousBlockQuote = previousAttributes[@"Attribute__Blockquote"]; + if (previousBlockQuote == nil) { + isFirstLine = true; + } else if (![blockQuote isEqual:previousBlockQuote]) { + isFirstLine = true; + } + } + + if (isFirstLine) { + result.size.width -= 100.0f; + } + } } } -#endif*/ return result; } diff --git a/submodules/AttachmentTextInputPanelNode/BUILD b/submodules/AttachmentTextInputPanelNode/BUILD index f82aeb9a52..5957166318 100644 --- a/submodules/AttachmentTextInputPanelNode/BUILD +++ b/submodules/AttachmentTextInputPanelNode/BUILD @@ -34,6 +34,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", + "//submodules/TelegramUI/Components/Chat/ChatInputTextNode", ], visibility = [ "//visibility:public", diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index 11f95b971a..570d1841d0 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -23,6 +23,7 @@ import LottieAnimationComponent import AnimationCache import MultiAnimationRenderer import TextNodeWithEntities +import ChatInputTextNode private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) private let minInputFontSize: CGFloat = 5.0 @@ -112,7 +113,7 @@ private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackground } } -private class CaptionEditableTextNode: EditableTextNode { +private class CaptionEditableTextNode: ChatInputTextNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let previousAlpha = self.alpha self.alpha = 1.0 @@ -190,7 +191,7 @@ final class CustomEmojiContainerView: UIView { } } -public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, ASEditableTextNodeDelegate { +public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate { private let context: AccountContext private let isCaption: Bool @@ -202,7 +203,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS private var textPlaceholderNode: ImmediateTextNode private let textInputContainerBackgroundNode: ASImageNode private let textInputContainer: ASDisplayNode - public var textInputNode: EditableTextNode? + public var textInputNode: ChatInputTextNode? private var dustNode: InvisibleInkDustNode? private var customEmojiContainerView: CustomEmojiContainerView? private var oneLineNode: TextNodeWithEntities @@ -249,7 +250,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS var storedInputLanguage: String? var effectiveInputLanguage: String? { if let textInputNode = textInputNode, textInputNode.isFirstResponder() { - return textInputNode.textInputMode.primaryLanguage + return textInputNode.textInputMode?.primaryLanguage } else { return self.storedInputLanguage } @@ -308,7 +309,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(baseFontSize), textColor: textColor) - self.editableTextNodeDidUpdateText(textInputNode) + self.chatInputTextNodeDidUpdateText() } } } @@ -516,7 +517,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS paragraphStyle.maximumLineHeight = 20.0 paragraphStyle.minimumLineHeight = 20.0 - textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor.rawValue: textColor, NSAttributedString.Key.paragraphStyle.rawValue: paragraphStyle] + textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.paragraphStyle: paragraphStyle] textInputNode.clipsToBounds = false textInputNode.textView.clipsToBounds = false textInputNode.delegate = self @@ -532,7 +533,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS textInputNode.textView.inputAssistantItem.trailingBarButtonGroups = [] if let presentationInterfaceState = self.presentationInterfaceState { - refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState) } @@ -541,6 +542,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) textInputNode.view.layoutIfNeeded() + textInputNode.textView.updateLayout(size: textInputNode.bounds.size) self.updateSpoiler() } @@ -587,8 +589,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS let textFieldHeight: CGFloat if let textInputNode = self.textInputNode { let maxTextWidth = width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - let measuredHeight = textInputNode.measure(CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude)) - let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight.height)) + let measuredHeight = textInputNode.textHeightForWidth(maxTextWidth) + let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight)) let maxNumberOfLines = min(12, (Int(fieldMaxHeight - 11.0) - 33) / 22) @@ -674,7 +676,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS textInputNode.attributedText = updatedText textInputNode.selectedRange = selectedRange } - textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor] + textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor: textColor] self.updateSpoiler() } @@ -960,11 +962,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } private var skipUpdate = false - @objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + public func chatInputTextNodeDidUpdateText() { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider) - refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) self.updateSpoiler() @@ -973,7 +975,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self.skipUpdate = true self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) - self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode.primaryLanguage }) + self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode?.primaryLanguage }) if self.isCaption, let presentationInterfaceState = self.presentationInterfaceState { self.presentationInterfaceState = presentationInterfaceState.updatedInterfaceState({ return $0.withUpdatedComposeInputState(inputTextState) @@ -988,6 +990,10 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } + @objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + self.chatInputTextNodeDidUpdateText() + } + private func updateSpoiler() { guard let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else { return @@ -1131,7 +1137,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS textInputNode.textView.isScrollEnabled = false - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.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.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider) @@ -1345,13 +1351,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } - @objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { + public func chatInputTextNodeShouldReturn() -> Bool { if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero { self.sendButtonPressed() } return false } + @objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { + return self.chatInputTextNodeShouldReturn() + } + private func applyUpdateSendButtonIcon() { if let interfaceState = self.presentationInterfaceState { let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil @@ -1371,7 +1381,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } - @objc public func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { + public func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) { if !dueToEditing && !self.updatingInputState { let inputTextState = self.inputTextState self.skipUpdate = true @@ -1385,13 +1395,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) self.updateSpoilersRevealed() } } - @objc public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + @objc public func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { + self.chatInputTextNodeDidChangeSelection(dueToEditing: dueToEditing) + } + + public func chatInputTextNodeDidBeginEditing() { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in return (.text, state.keyboardButtonsMessage?.id) }) @@ -1404,8 +1418,15 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } - public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { - self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage + @objc public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + self.chatInputTextNodeDidBeginEditing() + } + + public func chatInputTextNodeDidFinishEditing() { + guard let editableTextNode = self.textInputNode else { + return + } + self.storedInputLanguage = editableTextNode.textInputMode?.primaryLanguage self.inputMenu.deactivate() self.focusUpdated?(false) @@ -1415,6 +1436,10 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } + public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + self.chatInputTextNodeDidFinishEditing() + } + public func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? { if action == makeSelectorFromString("_accessibilitySpeak:") { if case .format = self.inputMenu.state { @@ -1461,8 +1486,12 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return nil } - @available(iOS 16.0, *) - public func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + @available(iOS 13.0, *) + public func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + guard let editableTextNode = self.textInputNode else { + return UIMenu(children: suggestedActions) + } + var actions = suggestedActions if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 { @@ -1520,6 +1549,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return UIMenu(children: actions) } + @available(iOS 16.0, *) + public func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + return self.chatInputTextNodeMenu(forTextRange: textRange, suggestedActions: suggestedActions) + } + private var currentSpeechHolder: SpeechSynthesizerHolder? @objc func _accessibilitySpeak(_ sender: Any) { var text = "" @@ -1612,7 +1646,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self.updateSpoilersRevealed(animated: animated) } - @objc public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + public func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + guard let editableTextNode = self.textInputNode else { + return true + } + var cleanText = text let removeSequences: [String] = ["\u{202d}", "\u{202c}"] for sequence in removeSequences { @@ -1645,7 +1683,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return true } - @objc public func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { + @objc public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + return self.chatInputTextNode(shouldChangeTextIn: range, replacementText: text) + } + + public func chatInputTextNodeShouldCopy() -> Bool { self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in storeInputTextInPasteboard(current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count))) return (current, inputMode) @@ -1653,7 +1695,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return false } - @objc public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + @objc public func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { + return self.chatInputTextNodeShouldCopy() + } + + public func chatInputTextNodeShouldPaste() -> Bool { let pasteboard = UIPasteboard.general var attributedString: NSAttributedString? @@ -1678,6 +1724,13 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return true } + @objc public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + return self.chatInputTextNodeShouldPaste() + } + + public func chatInputTextNodeBackspaceWhileEmpty() { + } + @objc func sendButtonPressed() { let inputTextMaxLength: Int32? if let maxCaptionLength = self.maxCaptionLength { diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 4af9b18d76..372fd7bde3 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -980,7 +980,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { sendWhenOnlineAvailable = false } - let controller = ChatSendMessageActionSheetController(context: strongSelf.context, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, attachment: true, canSendWhenOnline: sendWhenOnlineAvailable, completion: { + let controller = ChatSendMessageActionSheetController(context: strongSelf.context, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputView: textInputNode.textView, attachment: true, canSendWhenOnline: sendWhenOnlineAvailable, completion: { }, sendMessage: { [weak textInputPanelNode] mode in switch mode { case .generic: diff --git a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift index 94fc6d07ee..9335f083d7 100644 --- a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift +++ b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift @@ -259,10 +259,27 @@ public struct ChatInterfaceHistoryScrollState: Codable, Equatable { } public final class ChatInterfaceState: Codable, Equatable { + public struct ReplyMessageSubject: Codable, Equatable { + public var messageId: EngineMessage.Id + public var quote: EngineMessageReplyQuote? + + public init(messageId: EngineMessage.Id, quote: EngineMessageReplyQuote?) { + self.messageId = messageId + self.quote = quote + } + + public var subjectModel: EngineMessageReplySubject { + return EngineMessageReplySubject( + messageId: self.messageId, + quote: self.quote + ) + } + } + public let timestamp: Int32 public let composeInputState: ChatTextInputState public let composeDisableUrlPreview: String? - public let replyMessageId: EngineMessage.Id? + public let replyMessageSubject: ReplyMessageSubject? public let forwardMessageIds: [EngineMessage.Id]? public let forwardOptionsState: ChatInterfaceForwardOptionsState? public let editMessage: ChatEditMessageState? @@ -274,15 +291,20 @@ public final class ChatInterfaceState: Codable, Equatable { public let inputLanguage: String? public var synchronizeableInputState: SynchronizeableChatInputState? { - if self.composeInputState.inputText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && self.replyMessageId == nil { + if self.composeInputState.inputText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && self.replyMessageSubject == nil { return nil } else { - return SynchronizeableChatInputState(replyToMessageId: self.replyMessageId, text: self.composeInputState.inputText.string, entities: generateChatInputTextEntities(self.composeInputState.inputText), timestamp: self.timestamp, textSelection: self.composeInputState.selectionRange) + return SynchronizeableChatInputState(replySubject: self.replyMessageSubject?.subjectModel, text: self.composeInputState.inputText.string, entities: generateChatInputTextEntities(self.composeInputState.inputText), timestamp: self.timestamp, textSelection: self.composeInputState.selectionRange) } } public func withUpdatedSynchronizeableInputState(_ state: SynchronizeableChatInputState?) -> ChatInterfaceState { - var result = self.withUpdatedComposeInputState(ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(state?.text ?? "", entities: state?.entities ?? []))).withUpdatedReplyMessageId(state?.replyToMessageId) + var result = self.withUpdatedComposeInputState(ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(state?.text ?? "", entities: state?.entities ?? []))).withUpdatedReplyMessageSubject((state?.replySubject).flatMap { + return ReplyMessageSubject( + messageId: $0.messageId, + quote: $0.quote + ) + }) if let timestamp = state?.timestamp { result = result.withUpdatedTimestamp(timestamp) } @@ -305,7 +327,7 @@ public final class ChatInterfaceState: Codable, Equatable { self.timestamp = 0 self.composeInputState = ChatTextInputState() self.composeDisableUrlPreview = nil - self.replyMessageId = nil + self.replyMessageSubject = nil self.forwardMessageIds = nil self.forwardOptionsState = nil self.editMessage = nil @@ -317,11 +339,11 @@ public final class ChatInterfaceState: Codable, Equatable { self.inputLanguage = nil } - public init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreview: String?, replyMessageId: EngineMessage.Id?, forwardMessageIds: [EngineMessage.Id]?, forwardOptionsState: ChatInterfaceForwardOptionsState?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode, silentPosting: Bool, inputLanguage: String?) { + public init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreview: String?, replyMessageSubject: ReplyMessageSubject?, forwardMessageIds: [EngineMessage.Id]?, forwardOptionsState: ChatInterfaceForwardOptionsState?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode, silentPosting: Bool, inputLanguage: String?) { self.timestamp = timestamp self.composeInputState = composeInputState self.composeDisableUrlPreview = composeDisableUrlPreview - self.replyMessageId = replyMessageId + self.replyMessageSubject = replyMessageSubject self.forwardMessageIds = forwardMessageIds self.forwardOptionsState = forwardOptionsState self.editMessage = editMessage @@ -347,13 +369,18 @@ public final class ChatInterfaceState: Codable, Equatable { } else { self.composeDisableUrlPreview = nil } - let replyMessageIdPeerId: Int64? = try? container.decodeIfPresent(Int64.self, forKey: "r.p") - let replyMessageIdNamespace: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "r.n") - let replyMessageIdId: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "r.i") - if let replyMessageIdPeerId = replyMessageIdPeerId, let replyMessageIdNamespace = replyMessageIdNamespace, let replyMessageIdId = replyMessageIdId { - self.replyMessageId = EngineMessage.Id(peerId: EnginePeer.Id(replyMessageIdPeerId), namespace: replyMessageIdNamespace, id: replyMessageIdId) + + if let replyMessageSubject = try? container.decodeIfPresent(ReplyMessageSubject.self, forKey: "replyMessageSubject") { + self.replyMessageSubject = replyMessageSubject } else { - self.replyMessageId = nil + let replyMessageIdPeerId: Int64? = try? container.decodeIfPresent(Int64.self, forKey: "r.p") + let replyMessageIdNamespace: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "r.n") + let replyMessageIdId: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "r.i") + if let replyMessageIdPeerId = replyMessageIdPeerId, let replyMessageIdNamespace = replyMessageIdNamespace, let replyMessageIdId = replyMessageIdId { + self.replyMessageSubject = ReplyMessageSubject(messageId: EngineMessage.Id(peerId: EnginePeer.Id(replyMessageIdPeerId), namespace: replyMessageIdNamespace, id: replyMessageIdId), quote: nil) + } else { + self.replyMessageSubject = nil + } } if let forwardMessageIdsData = try? container.decodeIfPresent(Data.self, forKey: "fm") { self.forwardMessageIds = EngineMessage.Id.decodeArrayFromData(forwardMessageIdsData) @@ -400,14 +427,10 @@ public final class ChatInterfaceState: Codable, Equatable { } else { try container.encodeNil(forKey: "dup") } - if let replyMessageId = self.replyMessageId { - try container.encode(replyMessageId.peerId.toInt64(), forKey: "r.p") - try container.encode(replyMessageId.namespace, forKey: "r.n") - try container.encode(replyMessageId.id, forKey: "r.i") + if let replyMessageSubject = self.replyMessageSubject { + try container.encode(replyMessageSubject, forKey: "replyMessageSubject") } else { - try container.encodeNil(forKey: "r.p") - try container.encodeNil(forKey: "r.n") - try container.encodeNil(forKey: "r.i") + try container.encodeNil(forKey: "replyMessageSubject") } if let forwardMessageIds = self.forwardMessageIds { try container.encode(EngineMessage.Id.encodeArrayToData(forwardMessageIds), forKey: "fm") @@ -477,17 +500,17 @@ public final class ChatInterfaceState: Codable, Equatable { if lhs.inputLanguage != rhs.inputLanguage { return false } - return lhs.composeInputState == rhs.composeInputState && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState && lhs.editMessage == rhs.editMessage + return lhs.composeInputState == rhs.composeInputState && lhs.replyMessageSubject == rhs.replyMessageSubject && lhs.selectionState == rhs.selectionState && lhs.editMessage == rhs.editMessage } public func withUpdatedComposeInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { let updatedComposeInputState = inputState - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedComposeDisableUrlPreview(_ disableUrlPreview: String?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: disableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: disableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedEffectiveInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { @@ -499,19 +522,19 @@ public final class ChatInterfaceState: Codable, Equatable { updatedComposeInputState = inputState } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } - public func withUpdatedReplyMessageId(_ replyMessageId: EngineMessage.Id?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + public func withUpdatedReplyMessageSubject(_ replyMessageSubject: ReplyMessageSubject?) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedForwardMessageIds(_ forwardMessageIds: [EngineMessage.Id]?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedForwardOptionsState(_ forwardOptionsState: ChatInterfaceForwardOptionsState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedSelectedMessages(_ messageIds: [EngineMessage.Id]) -> ChatInterfaceState { @@ -522,7 +545,7 @@ public final class ChatInterfaceState: Codable, Equatable { for messageId in messageIds { selectedIds.insert(messageId) } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withToggledSelectedMessages(_ messageIds: [EngineMessage.Id], value: Bool) -> ChatInterfaceState { @@ -537,39 +560,39 @@ public final class ChatInterfaceState: Codable, Equatable { selectedIds.remove(messageId) } } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withoutSelectionState() -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedTimestamp(_ timestamp: Int32) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedEditMessage(_ editMessage: ChatEditMessageState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedMessageActionsState(_ f: (ChatInterfaceMessageActionsState) -> ChatInterfaceMessageActionsState) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState), historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState), historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedHistoryScrollState(_ historyScrollState: ChatInterfaceHistoryScrollState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedMediaRecordingMode(_ mediaRecordingMode: ChatTextInputMediaRecordingButtonMode) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedSilentPosting(_ silentPosting: Bool) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: silentPosting, inputLanguage: self.inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: silentPosting, inputLanguage: self.inputLanguage) } public func withUpdatedInputLanguage(_ inputLanguage: String?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: inputLanguage) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: inputLanguage) } public static func parse(_ state: OpaqueChatInterfaceState) -> ChatInterfaceState { diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift index 64f6df38e7..43fdbff026 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift @@ -22,7 +22,17 @@ public func chatTextInputAddFormattingAttribute(_ state: ChatTextInputState, att result.removeAttribute(attribute, range: nsRange) } if addAttribute { - result.addAttribute(attribute, value: true as Bool, range: nsRange) + if attribute == ChatTextInputAttributes.quote { + result.addAttribute(attribute, value: ChatTextInputTextQuoteAttribute(), range: nsRange) + if nsRange.upperBound != result.length && (result.string as NSString).character(at: nsRange.upperBound) != 0x0a { + result.insert(NSAttributedString(string: "\n"), at: nsRange.upperBound) + } + if nsRange.lowerBound != 0 && (result.string as NSString).character(at: nsRange.lowerBound - 1) != 0x0a { + result.insert(NSAttributedString(string: "\n"), at: nsRange.lowerBound) + } + } else { + result.addAttribute(attribute, value: true as Bool, range: nsRange) + } } return ChatTextInputState(inputText: result, selectionRange: state.selectionRange) } else { diff --git a/submodules/ChatSendMessageActionUI/BUILD b/submodules/ChatSendMessageActionUI/BUILD index 6f6c0bba51..c4a3e42239 100644 --- a/submodules/ChatSendMessageActionUI/BUILD +++ b/submodules/ChatSendMessageActionUI/BUILD @@ -20,6 +20,7 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/TextFormat:TextFormat", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/Chat/ChatInputTextNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index ffc5c78464..c36f64adf6 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -28,7 +28,7 @@ public final class ChatSendMessageActionSheetController: ViewController { private let gesture: ContextGesture private let sourceSendButton: ASDisplayNode - private let textInputNode: EditableTextNode + private let textInputView: UITextView private let attachment: Bool private let canSendWhenOnline: Bool private let completion: () -> Void @@ -46,7 +46,7 @@ public final class ChatSendMessageActionSheetController: ViewController { public var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id?, isScheduledMessages: Bool = false, forwardMessageIds: [EngineMessage.Id]?, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode) -> Void, schedule: @escaping () -> Void) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id?, isScheduledMessages: Bool = false, forwardMessageIds: [EngineMessage.Id]?, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode) -> Void, schedule: @escaping () -> Void) { self.context = context self.peerId = peerId self.isScheduledMessages = isScheduledMessages @@ -54,7 +54,7 @@ public final class ChatSendMessageActionSheetController: ViewController { self.hasEntityKeyboard = hasEntityKeyboard self.gesture = gesture self.sourceSendButton = sourceSendButton - self.textInputNode = textInputNode + self.textInputView = textInputView self.attachment = attachment self.canSendWhenOnline = canSendWhenOnline self.completion = completion @@ -107,7 +107,7 @@ public final class ChatSendMessageActionSheetController: ViewController { canSchedule = false } - self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, presentationData: self.presentationData, reminders: reminders, gesture: gesture, sourceSendButton: self.sourceSendButton, textInputNode: self.textInputNode, attachment: self.attachment, canSendWhenOnline: self.canSendWhenOnline, forwardedCount: forwardedCount, hasEntityKeyboard: self.hasEntityKeyboard, emojiViewProvider: self.emojiViewProvider, send: { [weak self] in + self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, presentationData: self.presentationData, reminders: reminders, gesture: gesture, sourceSendButton: self.sourceSendButton, textInputView: self.textInputView, attachment: self.attachment, canSendWhenOnline: self.canSendWhenOnline, forwardedCount: forwardedCount, hasEntityKeyboard: self.hasEntityKeyboard, emojiViewProvider: self.emojiViewProvider, send: { [weak self] in self?.sendMessage(.generic) self?.dismiss(cancel: false) }, sendSilently: { [weak self] in diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift index e5d88cd154..817b89d626 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift @@ -10,6 +10,7 @@ import AppBundle import ContextUI import TextFormat import EmojiTextAttachmentView +import ChatInputTextNode private let leftInset: CGFloat = 16.0 private let rightInset: CGFloat = 16.0 @@ -159,7 +160,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, private var presentationData: PresentationData private let sourceSendButton: ASDisplayNode private let textFieldFrame: CGRect - private let textInputNode: EditableTextNode + private let textInputView: UITextView private let attachment: Bool private let forwardedCount: Int? private let hasEntityKeyboard: Bool @@ -176,8 +177,8 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, private let messageClipNode: ASDisplayNode private let messageBackgroundNode: ASImageNode - private let fromMessageTextNode: EditableTextNode - private let toMessageTextNode: EditableTextNode + private let fromMessageTextNode: ChatInputTextNode + private let toMessageTextNode: ChatInputTextNode private let scrollNode: ASScrollNode private var fromCustomEmojiContainerView: CustomEmojiContainerView? @@ -193,12 +194,12 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? - init(context: AccountContext, presentationData: PresentationData, reminders: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, attachment: Bool, canSendWhenOnline: Bool, forwardedCount: Int?, hasEntityKeyboard: Bool, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, send: (() -> Void)?, sendSilently: (() -> Void)?, sendWhenOnline: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?) { + init(context: AccountContext, presentationData: PresentationData, reminders: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, attachment: Bool, canSendWhenOnline: Bool, forwardedCount: Int?, hasEntityKeyboard: Bool, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, send: (() -> Void)?, sendSilently: (() -> Void)?, sendWhenOnline: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?) { self.context = context self.presentationData = presentationData self.sourceSendButton = sourceSendButton - self.textFieldFrame = textInputNode.convert(textInputNode.bounds, to: nil) - self.textInputNode = textInputNode + self.textFieldFrame = textInputView.convert(textInputView.bounds, to: nil) + self.textInputView = textInputView self.attachment = attachment self.forwardedCount = forwardedCount self.hasEntityKeyboard = hasEntityKeyboard @@ -223,9 +224,9 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.messageClipNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) self.messageBackgroundNode = ASImageNode() self.messageBackgroundNode.isUserInteractionEnabled = true - self.fromMessageTextNode = EditableTextNode() + self.fromMessageTextNode = ChatInputTextNode() self.fromMessageTextNode.isUserInteractionEnabled = false - self.toMessageTextNode = EditableTextNode() + self.toMessageTextNode = ChatInputTextNode() self.toMessageTextNode.alpha = 0.0 self.toMessageTextNode.isUserInteractionEnabled = false @@ -259,7 +260,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.sendButtonNode.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside) - if let attributedText = textInputNode.attributedText, !attributedText.string.isEmpty { + if let attributedText = textInputView.attributedText, !attributedText.string.isEmpty { self.animateInputField = true self.fromMessageTextNode.attributedText = attributedText @@ -381,7 +382,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.updateTextContents(rects: customEmojiRects, textInputNode: self.toMessageTextNode, from: false) } - func updateTextContents(rects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)], textInputNode: EditableTextNode, from: Bool) { + func updateTextContents(rects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)], textInputNode: ChatInputTextNode, from: Bool) { if !rects.isEmpty { let customEmojiContainerView: CustomEmojiContainerView if from, let current = self.fromCustomEmojiContainerView { @@ -428,7 +429,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.contentContainerNode.backgroundColor = self.presentationData.theme.contextMenu.backgroundColor - if let toAttributedText = self.textInputNode.attributedText?.mutableCopy() as? NSMutableAttributedString { + if let toAttributedText = self.textInputView.attributedText?.mutableCopy() as? NSMutableAttributedString { toAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: self.presentationData.theme.chat.message.outgoing.primaryTextColor, range: NSMakeRange(0, (toAttributedText.string as NSString).length)) self.toMessageTextNode.attributedText = toAttributedText } @@ -447,7 +448,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, return } - self.textInputNode.textView.setContentOffset(self.textInputNode.textView.contentOffset, animated: false) + self.textInputView.setContentOffset(self.textInputView.contentOffset, animated: false) self.effectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -469,7 +470,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.sendButtonNode.layer.animatePosition(from: self.sendButtonFrame.center, to: self.sendButtonNode.position, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) var initialWidth = self.textFieldFrame.width + 32.0 - if self.textInputNode.textView.attributedText.string.isEmpty { + if self.textInputView.attributedText.string.isEmpty { initialWidth = ceil(layout.size.width - self.textFieldFrame.origin.x - self.sendButtonFrame.width - layout.safeInsets.left - layout.safeInsets.right + 21.0) } @@ -497,8 +498,8 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.messageBackgroundNode.layer.animatePosition(from: CGPoint(x: (initialWidth - self.messageClipNode.bounds.width) / 2.0, y: delta), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) var textXOffset: CGFloat = 0.0 - let textYOffset = self.textInputNode.textView.contentSize.height - self.textInputNode.textView.contentOffset.y - self.textInputNode.textView.frame.height - if self.textInputNode.textView.numberOfLines == 1 && self.textInputNode.isRTL { + let textYOffset = self.textInputView.contentSize.height - self.textInputView.contentOffset.y - self.textInputView.frame.height + if self.textInputView.numberOfLines == 1 && self.textInputView.isRTL { textXOffset = initialWidth - self.messageClipNode.bounds.width } self.fromMessageTextNode.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) @@ -513,7 +514,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, Queue.mainQueue().after(0.01, { if self.animateInputField { - self.textInputNode.isHidden = true + self.textInputView.isHidden = true } self.updateTextContents() }) @@ -537,7 +538,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, let intermediateCompletion: () -> Void = { [weak self] in if completedEffect && completedButton && completedBubble && completedAlpha && !completed { completed = true - self?.textInputNode.isHidden = false + self?.textInputView.isHidden = false self?.sourceSendButton.isHidden = false completion() } @@ -553,7 +554,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, Queue.mainQueue().after(0.45) { if !completed { completed = true - self.textInputNode.isHidden = false + self.textInputView.isHidden = false self.sourceSendButton.isHidden = false completion() } @@ -568,7 +569,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, intermediateCompletion() }) } else { - self.textInputNode.isHidden = false + self.textInputView.isHidden = false self.messageClipNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in completedAlpha = true intermediateCompletion() @@ -591,7 +592,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, } var initialWidth = self.textFieldFrame.width + 32.0 - if self.textInputNode.textView.attributedText.string.isEmpty { + if self.textInputView.attributedText.string.isEmpty { initialWidth = ceil(layout.size.width - self.textFieldFrame.origin.x - self.sendButtonFrame.width - layout.safeInsets.left - layout.safeInsets.right + 21.0) } @@ -619,8 +620,8 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.messageBackgroundNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: (initialWidth - self.messageClipNode.bounds.width) / 2.0, y: delta), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) var textXOffset: CGFloat = 0.0 - let textYOffset = self.textInputNode.textView.contentSize.height - self.textInputNode.textView.contentOffset.y - self.textInputNode.textView.frame.height - if self.textInputNode.textView.numberOfLines == 1 && self.textInputNode.isRTL { + let textYOffset = self.textInputView.contentSize.height - self.textInputView.contentOffset.y - self.textInputView.frame.height + if self.textInputView.numberOfLines == 1 && self.textInputView.isRTL { textXOffset = initialWidth - self.messageClipNode.bounds.width } self.fromMessageTextNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) @@ -705,19 +706,19 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, messageFrame.origin.y += menuHeightWithInset } - if self.textInputNode.textView.attributedText.string.isEmpty { + if self.textInputView.attributedText.string.isEmpty { messageFrame.size.width = ceil(layout.size.width - messageFrame.origin.x - sendButtonFrame.width - layout.safeInsets.left - layout.safeInsets.right + 8.0) } var messageOriginDelta: CGFloat = 0.0 - if self.textInputNode.textView.numberOfLines == 1 || self.textInputNode.textView.attributedText.string.isEmpty { + if self.textInputView.numberOfLines == 1 || self.textInputView.attributedText.string.isEmpty { let textWidth = min(self.toMessageTextNode.textView.sizeThatFits(layout.size).width + 36.0, messageFrame.width) messageOriginDelta = messageFrame.width - textWidth messageFrame.origin.x += messageOriginDelta messageFrame.size.width = textWidth } - let messageHeight = max(messageFrame.size.height, self.textInputNode.textView.contentSize.height + 2.0) + let messageHeight = max(messageFrame.size.height, self.textInputView.contentSize.height + 2.0) messageFrame.size.height = messageHeight transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) @@ -736,10 +737,10 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, var textFrame = self.textFieldFrame textFrame.origin = CGPoint(x: 13.0, y: 6.0 - UIScreenPixel) - textFrame.size.height = self.textInputNode.textView.contentSize.height - textFrame.size.width -= self.textInputNode.textContainerInset.right + textFrame.size.height = self.textInputView.contentSize.height + textFrame.size.width -= self.textInputView.textContainerInset.right - if self.textInputNode.isRTL { + if self.textInputView.isRTL { textFrame.origin.x -= messageOriginDelta } diff --git a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift b/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift index fc8bc56aab..91d354d021 100644 --- a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift +++ b/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift @@ -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, 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) } } else { strongSelf.textNode.attributedText = attributedText - refreshGenericTextInputAttributes(strongSelf.textNode, 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) } if strongSelf.backgroundNode.supernode == nil { @@ -577,7 +577,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { if let item = self.item { if let _ = self.textNode.attributedText { - refreshGenericTextInputAttributes(editableTextNode, 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) let updatedText = stateAttributedStringForText(self.textNode.attributedText!) item.textUpdated(updatedText) } else { @@ -606,8 +606,8 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe UIMenuController.shared.update() } - refreshChatTextInputTypingAttributes(editableTextNode, theme: item.presentationData.theme, baseFontSize: 17.0) - refreshGenericTextInputAttributes(editableTextNode, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) + refreshChatTextInputTypingAttributes(editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0) + refreshGenericTextInputAttributes(editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) } } @@ -657,8 +657,8 @@ private func chatTextInputAddFormattingAttribute(item: CreatePollTextInputItem, textNode.attributedText = result textNode.selectedRange = nsRange - refreshChatTextInputTypingAttributes(textNode, theme: theme, baseFontSize: 17.0) - refreshGenericTextInputAttributes(textNode, theme: theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) + refreshChatTextInputTypingAttributes(textNode.textView, theme: theme, baseFontSize: 17.0) + refreshGenericTextInputAttributes(textNode.textView, theme: theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil) let updatedText = stateAttributedStringForText(textNode.attributedText!) item.textUpdated(updatedText) diff --git a/submodules/Display/Source/EditableTextNode.swift b/submodules/Display/Source/EditableTextNode.swift index a5f7d38b4a..daf1fa18da 100644 --- a/submodules/Display/Source/EditableTextNode.swift +++ b/submodules/Display/Source/EditableTextNode.swift @@ -48,4 +48,20 @@ public extension UITextView { } return numberOfLines } + + var isRTL: Bool { + if let text = self.text, !text.isEmpty { + let tagger = NSLinguisticTagger(tagSchemes: [.language], options: 0) + tagger.string = text + + let lang = tagger.tag(at: 0, scheme: .language, tokenRange: nil, sentenceRange: nil) + if let lang = lang?.rawValue, lang.contains("he") || lang.contains("ar") || lang.contains("fa") { + return true + } else { + return false + } + } else { + return false + } + } } diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 5381fb5224..de32a60afb 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1108,9 +1108,9 @@ open class TextNode: ASDisplayNode { displayEmbeddedItemsUnderSpoilers: Bool, customTruncationToken: NSAttributedString? ) -> TextNodeLayout { - let blockQuoteLeftInset: CGFloat = 7.0 + let blockQuoteLeftInset: CGFloat = 9.0 let blockQuoteRightInset: CGFloat = 0.0 - let blockQuoteIconInset: CGFloat = 12.0 + let blockQuoteIconInset: CGFloat = 7.0 struct StringSegment { let title: NSAttributedString? @@ -1991,7 +1991,9 @@ open class TextNode: ASDisplayNode { for blockQuote in layout.blockQuotes { let radius: CGFloat = 3.0 - let blockFrame = blockQuote.frame.offsetBy(dx: offset.x + 2.0, dy: offset.y) + var blockFrame = blockQuote.frame.offsetBy(dx: offset.x + 2.0, dy: offset.y) + blockFrame.size.width += 4.0 + blockFrame.origin.x -= 2.0 context.setFillColor(blockQuote.tintColor.withMultipliedAlpha(0.1).cgColor) context.addPath(UIBezierPath(roundedRect: blockFrame, cornerRadius: radius).cgPath) @@ -2000,8 +2002,13 @@ open class TextNode: ASDisplayNode { context.setFillColor(blockQuote.tintColor.cgColor) let quoteRect = CGRect(origin: CGPoint(x: blockFrame.maxX - 4.0 - quoteIcon.size.width, y: blockFrame.minY + 4.0), size: quoteIcon.size) + context.saveGState() + context.translateBy(x: quoteRect.midX, y: quoteRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -quoteRect.midX, y: -quoteRect.midY) context.clip(to: quoteRect, mask: quoteIcon.cgImage!) context.fill(quoteRect) + context.restoreGState() context.resetClip() let lineFrame = CGRect(origin: CGPoint(x: blockFrame.minX, y: blockFrame.minY), size: CGSize(width: radius, height: blockFrame.height)) diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index e48e10fea8..3d0bfa4a68 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -531,6 +531,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll //component.controller()?.present(translateController, in: .window(.root)) self.controllerInteraction?.presentController(translateController, nil) }) + case .quote: + break } }) diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 95db9e40ee..b077375dae 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -136,7 +136,7 @@ private let boldItalicFont = Font.semiboldItalic(16.0) private let fixedFont = UIFont(name: "Menlo-Regular", size: 15.0) ?? textFont public func galleryCaptionStringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], message: Message?) -> NSAttributedString { - return stringWithAppliedEntities(text, entities: entities, baseColor: .white, linkColor: UIColor(rgb: 0x5ac8fa), baseFont: textFont, linkFont: textFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: fixedFont, blockQuoteFont: textFont, underlineLinks: false, message: message) + return stringWithAppliedEntities(text, entities: entities, baseColor: .white, linkColor: UIColor(rgb: 0x5ac8fa), baseFont: textFont, linkFont: textFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: fixedFont, blockQuoteFont: textFont, underlineLinks: false, message: message, adjustQuoteFontSize: true) } private func galleryMessageCaptionText(_ message: Message) -> String { diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index ae79240647..d817ee2d4f 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -172,7 +172,7 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDel let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, isCentered: false)) - let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, isCentered: false)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 7c06b37f1a..115659807c 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -436,7 +436,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, isCentered: false)) - let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, isCentered: false)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index 6b5749db51..524b33a5c0 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -1048,7 +1048,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate messages[message4.id] = message4 sampleMessages.append(message4) - let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil, quote: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages[message5.id] = message5 sampleMessages.append(message5) @@ -1059,7 +1059,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message6) - let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil, quote: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message7) let message8 = Message(stableId: 8, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 8), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66007, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index a79f4d6c36..3f1a10d53b 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -599,7 +599,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { messages[message4.id] = message4 sampleMessages.append(message4) - let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil, quote: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages[message5.id] = message5 sampleMessages.append(message5) @@ -610,7 +610,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message6) - let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil, quote: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message7) let message8 = Message(stableId: 8, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 8), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66007, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift index 2e7d5b3a82..4c557d932c 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift @@ -159,7 +159,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) } - let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, isCentered: false)) } diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 81c93c9b7a..bd53ea15d9 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -1882,9 +1882,11 @@ public final class ShareController: ViewController { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: url + "\n\n" + text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: url + "\n\n" + text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId.flatMap { + EngineMessageReplySubject(messageId: $0, quote: nil) + }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } else { - messages.append(.message(text: url, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: url, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) @@ -1915,9 +1917,9 @@ public final class ShareController: ViewController { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } - messages.append(.message(text: string, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: string, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -1947,12 +1949,12 @@ public final class ShareController: ViewController { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } let attributedText = NSMutableAttributedString(string: string, attributes: [ChatTextInputAttributes.italic: true as NSNumber]) attributedText.append(NSAttributedString(string: "\n\n\(url)")) let entities = generateChatInputTextEntities(attributedText) - messages.append(.message(text: attributedText.string, attributes: [TextEntitiesMessageAttribute(entities: entities)], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: attributedText.string, attributes: [TextEntitiesMessageAttribute(entities: entities)], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -1981,7 +1983,7 @@ public final class ShareController: ViewController { } var messages: [EnqueueMessage] = [] - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])), replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])), replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -2056,9 +2058,9 @@ public final class ShareController: ViewController { var messages: [EnqueueMessage] = [] if !text.isEmpty && !sendTextAsCaption { - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } - messages.append(.message(text: sendTextAsCaption ? text : "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: sendTextAsCaption ? text : "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -2088,9 +2090,9 @@ public final class ShareController: ViewController { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } - messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -2122,7 +2124,7 @@ public final class ShareController: ViewController { return .fail(.generic) } - messagesToEnqueue.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messagesToEnqueue.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } for message in messages { for media in message.media { diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 1e386035b7..e8b404aaee 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -565,10 +565,13 @@ extension StoreMessage { switch replyTo { case let .messageReplyHeader(flags, replyToMsgId, replyToPeerId, replyHeader, replyToTopId, quoteText, quoteEntities): let _ = replyHeader - let _ = quoteText - let _ = quoteEntities let isForumTopic = (flags & (1 << 3)) != 0 + var quote: EngineMessageReplyQuote? + if let quoteText = quoteText { + quote = EngineMessageReplyQuote(text: quoteText, entities: messageTextEntitiesFromApiEntities(quoteEntities ?? [])) + } + if let replyToMsgId = replyToMsgId { let replyPeerId = replyToPeerId?.peerId ?? peerId if let replyToTopId = replyToTopId { @@ -600,7 +603,7 @@ extension StoreMessage { threadId = makeMessageThreadId(threadIdValue) } } - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId)) + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote)) } case let .messageReplyStoryHeader(userId, storyId): attributes.append(ReplyStoryAttribute(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: storyId))) @@ -835,8 +838,11 @@ extension StoreMessage { switch replyTo { case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyToTopId, quoteText, quoteEntities): let _ = replyHeader - let _ = quoteText - let _ = quoteEntities + + var quote: EngineMessageReplyQuote? + if let quoteText = quoteText { + quote = EngineMessageReplyQuote(text: quoteText, entities: messageTextEntitiesFromApiEntities(quoteEntities ?? [])) + } if let replyToMsgId = replyToMsgId { let replyPeerId = replyToPeerId?.peerId ?? peerId @@ -857,7 +863,7 @@ extension StoreMessage { default: break } - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId)) + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote)) } 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/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 2d708c6182..4c2abb708a 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -9,11 +9,31 @@ public enum EnqueueMessageGrouping { case auto } +public struct EngineMessageReplyQuote: Codable, Equatable { + public var text: String + public var entities: [MessageTextEntity] + + public init(text: String, entities: [MessageTextEntity]) { + self.text = text + self.entities = entities + } +} + +public struct EngineMessageReplySubject: Codable, Equatable { + public var messageId: EngineMessage.Id + public var quote: EngineMessageReplyQuote? + + public init(messageId: EngineMessage.Id, quote: EngineMessageReplyQuote?) { + self.messageId = messageId + self.quote = quote + } +} + public enum EnqueueMessage { - case message(text: String, attributes: [MessageAttribute], inlineStickers: [MediaId: Media], mediaReference: AnyMediaReference?, replyToMessageId: MessageId?, replyToStoryId: StoryId?, localGroupingKey: Int64?, correlationId: Int64?, bubbleUpEmojiOrStickersets: [ItemCollectionId]) + case message(text: String, attributes: [MessageAttribute], inlineStickers: [MediaId: Media], mediaReference: AnyMediaReference?, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, localGroupingKey: Int64?, correlationId: Int64?, bubbleUpEmojiOrStickersets: [ItemCollectionId]) case forward(source: MessageId, threadId: Int64?, grouping: EnqueueMessageGrouping, attributes: [MessageAttribute], correlationId: Int64?) - public func withUpdatedReplyToMessageId(_ replyToMessageId: MessageId?) -> EnqueueMessage { + public func withUpdatedReplyToMessageId(_ replyToMessageId: EngineMessageReplySubject?) -> EnqueueMessage { switch self { case let .message(text, attributes, inlineStickers, mediaReference, _, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): return .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) @@ -266,9 +286,9 @@ public func enqueueMessagesToMultiplePeers(account: Account, peerIds: [PeerId], return account.postbox.transaction { transaction -> [MessageId] in var messageIds: [MessageId] = [] for peerId in peerIds { - var replyToMessageId: MessageId? + var replyToMessageId: EngineMessageReplySubject? if let threadIds = threadIds[peerId] { - replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadIds)) + replyToMessageId = EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadIds)), quote: nil) } var messages = messages if let replyToMessageId = replyToMessageId { @@ -295,13 +315,13 @@ public func resendMessages(account: Account, messageIds: [MessageId]) -> Signal< removeMessageIds.append(id) var filteredAttributes: [MessageAttribute] = [] - var replyToMessageId: MessageId? + var replyToMessageId: EngineMessageReplySubject? var replyToStoryId: StoryId? var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] var forwardSource: MessageId? inner: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - replyToMessageId = attribute.messageId + replyToMessageId = EngineMessageReplySubject(messageId: attribute.messageId, quote: attribute.quote) } else if let attribute = attribute as? ReplyStoryAttribute { replyToStoryId = attribute.storyId } else if let attribute = attribute as? OutgoingMessageInfoAttribute { @@ -349,7 +369,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } switch message { case let .message(_, attributes, _, _, replyToMessageId, _, _, _, _): - if let replyToMessageId = replyToMessageId, replyToMessageId.peerId != peerId, let replyMessage = transaction.getMessage(replyToMessageId) { + if let replyToMessageId = replyToMessageId, replyToMessageId.messageId.peerId != peerId, let replyMessage = transaction.getMessage(replyToMessageId.messageId) { var canBeForwarded = true if replyMessage.id.namespace != Namespaces.Message.Cloud { canBeForwarded = false @@ -361,7 +381,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } } if canBeForwarded { - updatedMessages.append((true, .forward(source: replyToMessageId, threadId: nil, grouping: .none, attributes: attributes, correlationId: nil))) + updatedMessages.append((true, .forward(source: replyToMessageId.messageId, threadId: nil, grouping: .none, attributes: attributes, correlationId: nil))) } } case let .forward(sourceId, threadId, _, _, _): @@ -372,7 +392,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, mediaReference = .standalone(media: media) } } - updatedMessages.append((transformedMedia, .message(text: sourceMessage.text, attributes: sourceMessage.attributes, inlineStickers: [:], mediaReference: mediaReference, replyToMessageId: threadId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: $0)) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))) + updatedMessages.append((transformedMedia, .message(text: sourceMessage.text, attributes: sourceMessage.attributes, inlineStickers: [:], mediaReference: mediaReference, replyToMessageId: threadId.flatMap { EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: $0)), quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))) continue outer } } @@ -483,12 +503,12 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, attributes.append(AutoremoveTimeoutMessageAttribute(timeout: peerAutoremoveTimeout, countdownBeginTime: nil)) } - if let replyToMessageId = replyToMessageId, replyToMessageId.peerId == peerId { + if let replyToMessageId = replyToMessageId, replyToMessageId.messageId.peerId == peerId { var threadMessageId: MessageId? - if let replyMessage = transaction.getMessage(replyToMessageId) { + if let replyMessage = transaction.getMessage(replyToMessageId.messageId) { threadMessageId = replyMessage.effectiveReplyThreadMessageId } - attributes.append(ReplyMessageAttribute(messageId: replyToMessageId, threadMessageId: threadMessageId)) + attributes.append(ReplyMessageAttribute(messageId: replyToMessageId.messageId, threadMessageId: threadMessageId, quote: replyToMessageId.quote)) } if let replyToStoryId = replyToStoryId { attributes.append(ReplyStoryAttribute(storyId: replyToStoryId)) @@ -604,21 +624,21 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, var threadId: Int64? if let replyToMessageId = replyToMessageId { - if let message = transaction.getMessage(replyToMessageId) { + if let message = transaction.getMessage(replyToMessageId.messageId) { if let threadIdValue = message.threadId { if threadIdValue == 1 { if let channel = transaction.getPeer(message.id.peerId) as? TelegramChannel, channel.flags.contains(.isForum) { threadId = threadIdValue } else { if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { - threadId = makeMessageThreadId(replyToMessageId) + threadId = makeMessageThreadId(replyToMessageId.messageId) } } } else { threadId = threadIdValue } } else if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { - threadId = makeMessageThreadId(replyToMessageId) + threadId = makeMessageThreadId(replyToMessageId.messageId) } } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 0ea13bff7c..bfc7e28cbe 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -164,7 +164,7 @@ public func standaloneSendEnqueueMessages( } if let replyToMessageId = message.replyToMessageId { - attributes.append(ReplyMessageAttribute(messageId: replyToMessageId, threadMessageId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyToMessageId, threadMessageId: nil, quote: nil)) } if let forwardOptions = message.forwardOptions { attributes.append(ForwardOptionsMessageAttribute(hideNames: forwardOptions.hideNames, hideCaptions: forwardOptions.hideCaptions)) diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 2d7a71f7db..e7b968033a 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1524,23 +1524,32 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: case .draftMessageEmpty: inputState = nil case let .draftMessage(_, replyToMsgHeader, message, entities, date): - var replyToMessageId: MessageId? + var replySubject: EngineMessageReplySubject? if let replyToMsgHeader = replyToMsgHeader { switch replyToMsgHeader { - case let .messageReplyHeader(flags, replyToMsgId, replyToPeerId, replyHeader, replyToTopId, quoteText, quoteEntities): - let _ = flags + case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyToTopId, quoteText, quoteEntities): let _ = replyHeader let _ = replyToTopId - let _ = quoteText - let _ = quoteEntities + + var quote: EngineMessageReplyQuote? + if let quoteText = quoteText { + quote = EngineMessageReplyQuote( + text: quoteText, + entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []) + ) + } + if let replyToMsgId = replyToMsgId { - replyToMessageId = MessageId(peerId: replyToPeerId?.peerId ?? peer.peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) + replySubject = EngineMessageReplySubject( + messageId: MessageId(peerId: replyToPeerId?.peerId ?? peer.peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), + quote: quote + ) } case .messageReplyStoryHeader: break } } - inputState = SynchronizeableChatInputState(replyToMessageId: replyToMessageId, text: message, entities: messageTextEntitiesFromApiEntities(entities ?? []), timestamp: date, textSelection: nil) + inputState = SynchronizeableChatInputState(replySubject: replySubject, text: message, entities: messageTextEntitiesFromApiEntities(entities ?? []), timestamp: date, textSelection: nil) } var threadId: Int64? if let topMsgId = topMsgId { diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift index a6f4aab0ea..103078dda9 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift @@ -151,15 +151,59 @@ private func synchronizeChatInputState(transaction: Transaction, postbox: Postbo } var replyTo: Api.InputReplyTo? - if inputState?.replyToMessageId != nil || topMsgId != nil { + if let replySubject = inputState?.replySubject { flags |= 1 << 0 var innerFlags: Int32 = 0 - if topMsgId != nil { - innerFlags |= 1 << 0 - } //inputReplyToMessage#73ec805 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector = InputReplyTo; - replyTo = .inputReplyToMessage(flags: innerFlags, replyToMsgId: inputState?.replyToMessageId?.id ?? topMsgId ?? 0, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) + var replyToPeer: Api.InputPeer? + var discard = false + if replySubject.messageId.peerId != peerId { + replyToPeer = transaction.getPeer(replySubject.messageId.peerId).flatMap(apiInputPeer) + if replyToPeer == nil { + discard = true + } + } + + var quoteText: String? + var quoteEntities: [Api.MessageEntity]? + if let replyQuote = replySubject.quote { + quoteText = replyQuote.text + + if !replyQuote.entities.isEmpty { + var associatedPeers = SimpleDictionary() + for entity in replyQuote.entities { + for associatedPeerId in entity.associatedPeerIds { + if associatedPeers[associatedPeerId] == nil { + if let associatedPeer = transaction.getPeer(associatedPeerId) { + associatedPeers[associatedPeerId] = associatedPeer + } + } + } + } + quoteEntities = apiEntitiesFromMessageTextEntities(replyQuote.entities, associatedPeers: associatedPeers) + } + } + + if replyToPeer != nil { + innerFlags |= 1 << 1 + } + if quoteText != nil { + innerFlags |= 1 << 2 + } + if quoteEntities != nil { + innerFlags |= 1 << 3 + } + + if !discard { + replyTo = .inputReplyToMessage(flags: innerFlags, replyToMsgId: replySubject.messageId.id, topMsgId: topMsgId, replyToPeerId: replyToPeer, quoteText: quoteText, quoteEntities: quoteEntities) + } + } else if let topMsgId = topMsgId { + flags |= 1 << 0 + + var innerFlags: Int32 = 0 + innerFlags |= 1 << 0 + replyTo = .inputReplyToMessage(flags: innerFlags, replyToMsgId: topMsgId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } return network.request(Api.functions.messages.saveDraft(flags: flags, replyTo: replyTo, peer: inputPeer, message: inputState?.text ?? "", entities: apiEntitiesFromMessageTextEntities(inputState?.entities ?? [], associatedPeers: SimpleDictionary()))) diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index 95fbe613b6..6f4e4659c5 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -779,6 +779,7 @@ public final class PendingMessageManager { var hideSendersNames = false var hideCaptions = false var replyMessageId: Int32? + var replyQuote: EngineMessageReplyQuote? var replyToStoryId: StoryId? var scheduleTime: Int32? var sendAsPeerId: PeerId? @@ -788,6 +789,7 @@ public final class PendingMessageManager { for attribute in messages[0].0.attributes { if let replyAttribute = attribute as? ReplyMessageAttribute { replyMessageId = replyAttribute.messageId.id + replyQuote = replyAttribute.quote } else if let attribute = attribute as? ReplyStoryAttribute { replyToStoryId = attribute.storyId } else if let _ = attribute as? ForwardSourceInfoAttribute { @@ -931,7 +933,30 @@ public final class PendingMessageManager { if topMsgId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) + + var quoteText: String? + var quoteEntities: [Api.MessageEntity]? + if let replyQuote = replyQuote { + replyFlags |= 1 << 2 + quoteText = replyQuote.text + + if !replyQuote.entities.isEmpty { + replyFlags |= 1 << 3 + var associatedPeers = SimpleDictionary() + for entity in replyQuote.entities { + for associatedPeerId in entity.associatedPeerIds { + if associatedPeers[associatedPeerId] == nil { + if let associatedPeer = transaction.getPeer(associatedPeerId) { + associatedPeers[associatedPeerId] = associatedPeer + } + } + } + } + quoteEntities = apiEntitiesFromMessageTextEntities(replyQuote.entities, associatedPeers: associatedPeers) + } + } + + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: quoteText, quoteEntities: quoteEntities) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -1108,6 +1133,7 @@ public final class PendingMessageManager { var forwardSourceInfoAttribute: ForwardSourceInfoAttribute? var messageEntities: [Api.MessageEntity]? var replyMessageId: Int32? + var replyQuote: EngineMessageReplyQuote? var replyToStoryId: StoryId? var scheduleTime: Int32? var sendAsPeerId: PeerId? @@ -1118,6 +1144,7 @@ public final class PendingMessageManager { for attribute in message.attributes { if let replyAttribute = attribute as? ReplyMessageAttribute { replyMessageId = replyAttribute.messageId.id + replyQuote = replyAttribute.quote } else if let attribute = attribute as? ReplyStoryAttribute { replyToStoryId = attribute.storyId } else if let outgoingInfo = attribute as? OutgoingMessageInfoAttribute { @@ -1178,7 +1205,30 @@ public final class PendingMessageManager { if message.threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil) + + var quoteText: String? + var quoteEntities: [Api.MessageEntity]? + if let replyQuote = replyQuote { + replyFlags |= 1 << 2 + quoteText = replyQuote.text + + if !replyQuote.entities.isEmpty { + replyFlags |= 1 << 3 + var associatedPeers = SimpleDictionary() + for entity in replyQuote.entities { + for associatedPeerId in entity.associatedPeerIds { + if associatedPeers[associatedPeerId] == nil { + if let associatedPeer = transaction.getPeer(associatedPeerId) { + associatedPeers[associatedPeerId] = associatedPeer + } + } + } + } + quoteEntities = apiEntitiesFromMessageTextEntities(replyQuote.entities, associatedPeers: associatedPeers) + } + } + + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: quoteText, quoteEntities: quoteEntities) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -1200,7 +1250,30 @@ public final class PendingMessageManager { if message.threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil) + + var quoteText: String? + var quoteEntities: [Api.MessageEntity]? + if let replyQuote = replyQuote { + replyFlags |= 1 << 2 + quoteText = replyQuote.text + + if !replyQuote.entities.isEmpty { + replyFlags |= 1 << 3 + var associatedPeers = SimpleDictionary() + for entity in replyQuote.entities { + for associatedPeerId in entity.associatedPeerIds { + if associatedPeers[associatedPeerId] == nil { + if let associatedPeer = transaction.getPeer(associatedPeerId) { + associatedPeers[associatedPeerId] = associatedPeer + } + } + } + } + quoteEntities = apiEntitiesFromMessageTextEntities(replyQuote.entities, associatedPeers: associatedPeers) + } + } + + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: quoteText, quoteEntities: quoteEntities) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -1236,7 +1309,30 @@ public final class PendingMessageManager { if message.threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil) + + var quoteText: String? + var quoteEntities: [Api.MessageEntity]? + if let replyQuote = replyQuote { + replyFlags |= 1 << 2 + quoteText = replyQuote.text + + if !replyQuote.entities.isEmpty { + replyFlags |= 1 << 3 + var associatedPeers = SimpleDictionary() + for entity in replyQuote.entities { + for associatedPeerId in entity.associatedPeerIds { + if associatedPeers[associatedPeerId] == nil { + if let associatedPeer = transaction.getPeer(associatedPeerId) { + associatedPeers[associatedPeerId] = associatedPeer + } + } + } + } + quoteEntities = apiEntitiesFromMessageTextEntities(replyQuote.entities, associatedPeers: associatedPeers) + } + } + + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: quoteText, quoteEntities: quoteEntities) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 diff --git a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift index 1151ec65f2..f110c457ca 100644 --- a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift +++ b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift @@ -881,7 +881,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { - attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil)) } var entitiesAttribute: TextEntitiesMessageAttribute? @@ -1113,7 +1113,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { - attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil)) } var entitiesAttribute: TextEntitiesMessageAttribute? @@ -1392,7 +1392,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { - attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil)) } var entitiesAttribute: TextEntitiesMessageAttribute? @@ -1593,7 +1593,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { - attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil)) } var entitiesAttribute: TextEntitiesMessageAttribute? diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift index c3b12f7f43..5fa3b57a2c 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift @@ -4,14 +4,16 @@ import Postbox public class ReplyMessageAttribute: MessageAttribute { public let messageId: MessageId public let threadMessageId: MessageId? + public let quote: EngineMessageReplyQuote? public var associatedMessageIds: [MessageId] { return [self.messageId] } - public init(messageId: MessageId, threadMessageId: MessageId?) { + public init(messageId: MessageId, threadMessageId: MessageId?, quote: EngineMessageReplyQuote?) { self.messageId = messageId self.threadMessageId = threadMessageId + self.quote = quote } required public init(decoder: PostboxDecoder) { @@ -23,6 +25,8 @@ public class ReplyMessageAttribute: MessageAttribute { } else { self.threadMessageId = nil } + + self.quote = decoder.decodeCodable(EngineMessageReplyQuote.self, forKey: "qu") } public func encode(_ encoder: PostboxEncoder) { @@ -34,6 +38,11 @@ public class ReplyMessageAttribute: MessageAttribute { encoder.encodeInt64(threadNamespaceAndId, forKey: "ti") encoder.encodeInt64(threadMessageId.peerId.toInt64(), forKey: "tp") } + if let quote = self.quote { + encoder.encodeCodable(quote, forKey: "qu") + } else { + encoder.encodeNil(forKey: "qu") + } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift index 6c9bd4eda0..13dcc3e32a 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift @@ -2,14 +2,14 @@ import Foundation import Postbox public struct SynchronizeableChatInputState: Codable, Equatable { - public let replyToMessageId: MessageId? + public let replySubject: EngineMessageReplySubject? public let text: String public let entities: [MessageTextEntity] public let timestamp: Int32 public let textSelection: Range? - public init(replyToMessageId: MessageId?, text: String, entities: [MessageTextEntity], timestamp: Int32, textSelection: Range?) { - self.replyToMessageId = replyToMessageId + public init(replySubject: EngineMessageReplySubject?, text: String, entities: [MessageTextEntity], timestamp: Int32, textSelection: Range?) { + self.replySubject = replySubject self.text = text self.entities = entities self.timestamp = timestamp @@ -22,10 +22,14 @@ public struct SynchronizeableChatInputState: Codable, Equatable { self.entities = (try? container.decode([MessageTextEntity].self, forKey: "e")) ?? [] self.timestamp = (try? container.decode(Int32.self, forKey: "s")) ?? 0 - if let messageIdPeerId = try? container.decodeIfPresent(Int64.self, forKey: "m.p"), let messageIdNamespace = try? container.decodeIfPresent(Int32.self, forKey: "m.n"), let messageIdId = try? container.decodeIfPresent(Int32.self, forKey: "m.i") { - self.replyToMessageId = MessageId(peerId: PeerId(messageIdPeerId), namespace: messageIdNamespace, id: messageIdId) + if let replySubject = try? container.decodeIfPresent(EngineMessageReplySubject.self, forKey: "rep") { + self.replySubject = replySubject } else { - self.replyToMessageId = nil + if let messageIdPeerId = try? container.decodeIfPresent(Int64.self, forKey: "m.p"), let messageIdNamespace = try? container.decodeIfPresent(Int32.self, forKey: "m.n"), let messageIdId = try? container.decodeIfPresent(Int32.self, forKey: "m.i") { + self.replySubject = EngineMessageReplySubject(messageId: MessageId(peerId: PeerId(messageIdPeerId), namespace: messageIdNamespace, id: messageIdId), quote: nil) + } else { + self.replySubject = nil + } } self.textSelection = nil } @@ -36,19 +40,11 @@ public struct SynchronizeableChatInputState: Codable, Equatable { try container.encode(self.text, forKey: "t") try container.encode(self.entities, forKey: "e") try container.encode(self.timestamp, forKey: "s") - if let replyToMessageId = self.replyToMessageId { - try container.encode(replyToMessageId.peerId.toInt64(), forKey: "m.p") - try container.encode(replyToMessageId.namespace, forKey: "m.n") - try container.encode(replyToMessageId.id, forKey: "m.i") - } else { - try container.encodeNil(forKey: "m.p") - try container.encodeNil(forKey: "m.n") - try container.encodeNil(forKey: "m.i") - } + try container.encodeIfPresent(self.replySubject, forKey: "rep") } public static func ==(lhs: SynchronizeableChatInputState, rhs: SynchronizeableChatInputState) -> Bool { - if lhs.replyToMessageId != rhs.replyToMessageId { + if lhs.replySubject != rhs.replySubject { return false } if lhs.text != rhs.text { @@ -103,7 +99,7 @@ func _internal_updateChatInputState(transaction: Transaction, peerId: PeerId, th let storedState = StoredPeerChatInterfaceState( overrideChatTimestamp: inputState?.timestamp, historyScrollMessageIndex: previousState?.historyScrollMessageIndex, - associatedMessageIds: (inputState?.replyToMessageId).flatMap({ [$0] }) ?? [], + associatedMessageIds: (inputState?.replySubject?.messageId).flatMap({ [$0] }) ?? [], data: updatedStateData ) if let threadId = threadId { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift index 14a3e6825c..9db50ceaf1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift @@ -2,7 +2,7 @@ import Foundation import Postbox import SwiftSignalKit -func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: MessageId?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> Bool { +func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> Bool { guard let message = _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) else { return false } @@ -10,10 +10,10 @@ func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to return true } -func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: MessageId?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { +func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { var replyToMessageId = replyToMessageId if replyToMessageId == nil, let threadId = threadId { - replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: MessageId.Id(clamping: threadId)) + replyToMessageId = EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: MessageId.Id(clamping: threadId)), quote: nil) } var attributes: [MessageAttribute] = [] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 9764d49673..10407f9cd2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -225,7 +225,7 @@ public extension TelegramEngine { public func enqueueOutgoingMessage( to peerId: EnginePeer.Id, - replyTo replyToMessageId: EngineMessage.Id?, + replyTo replyToMessageId: EngineMessageReplySubject?, storyId: StoryId? = nil, content: EngineOutgoingMessageContent, silentPosting: Bool = false, @@ -282,11 +282,11 @@ public extension TelegramEngine { ) } - public func enqueueOutgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: MessageId? = nil, replyToStoryId: StoryId? = nil, hideVia: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, correlationId: Int64? = nil) -> Bool { + public func enqueueOutgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject? = nil, replyToStoryId: StoryId? = nil, hideVia: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, correlationId: Int64? = nil) -> Bool { return _internal_enqueueOutgoingMessageWithChatContextResult(account: self.account, to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) } - public func outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: MessageId?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { + public func outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { return _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index fa19c0b2ee..27a4df588b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -865,7 +865,7 @@ public extension TelegramEngine { let storedState = StoredPeerChatInterfaceState( overrideChatTimestamp: state.synchronizeableInputState?.timestamp, historyScrollMessageIndex: state.historyScrollMessageIndex, - associatedMessageIds: (state.synchronizeableInputState?.replyToMessageId).flatMap({ [$0] }) ?? [], + associatedMessageIds: (state.synchronizeableInputState?.replySubject?.messageId).flatMap({ [$0] }) ?? [], data: data ) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 42845c0d51..768f885afb 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -296,6 +296,8 @@ public enum PresentationResourceKey: Int32 { case storyViewListLikeIcon case navigationPostStoryIcon + + case chatReplyBackgroundTemplateImage } public enum ChatExpiredStoryIndicatorType: Hashable { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 9e38d8bdd6..8af17d9eb5 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1284,4 +1284,23 @@ public struct PresentationResourcesChat { return generateTintedImage(image: UIImage(bundleImageName: "Stories/InputLikeOn"), color: UIColor(rgb: 0xFF3B30)) }) } + + public static func chatReplyBackgroundTemplateImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatReplyBackgroundTemplateImage.rawValue, { theme in + let radius: CGFloat = 3.0 + + return generateImage(CGSize(width: radius * 2.0 + 1.0, height: radius * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: radius).cgPath) + context.clip() + + context.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: radius, height: size.height))) + })?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius)).withRenderingMode(.alwaysTemplate) + }) + } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 81942f86a7..7cebb1e04f 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -346,6 +346,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", "//submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode", "//submodules/TelegramUI/Components/Chat/EditableTokenListNode", + "//submodules/TelegramUI/Components/Chat/ChatInputTextNode", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/BUILD new file mode 100644 index 0000000000..61f5f69ee3 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatInputTextNode", + module_name = "ChatInputTextNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/AppBundle", + "//submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/BUILD b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/BUILD new file mode 100644 index 0000000000..a588d9c601 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/BUILD @@ -0,0 +1,23 @@ + +objc_library( + name = "ChatInputTextViewImpl", + enable_modules = True, + module_name = "ChatInputTextViewImpl", + srcs = glob([ + "Sources/**/*.m", + "Sources/**/*.c", + "Sources/**/*.h", + ]), + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + sdk_frameworks = [ + "Foundation", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h new file mode 100755 index 0000000000..5bf0ee7a66 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h @@ -0,0 +1,25 @@ +#ifndef ChatInputTextViewImpl_h +#define ChatInputTextViewImpl_h + +#import +#import + +@interface ChatInputTextViewImplTargetForAction: NSObject + +@property (nonatomic, strong, readonly) id _Nullable target; + +- (instancetype _Nonnull)initWithTarget:(id _Nullable)target; + +@end + +@interface ChatInputTextViewImpl : UITextView + +@property (nonatomic, copy) bool (^ _Nullable shouldCopy)(); +@property (nonatomic, copy) bool (^ _Nullable shouldPaste)(); +@property (nonatomic, copy) ChatInputTextViewImplTargetForAction * _Nullable (^ _Nullable targetForActionImpl)(SEL _Nullable); +@property (nonatomic, copy) bool (^ _Nullable shouldReturn)(); +@property (nonatomic, copy) void (^ _Nullable backspaceWhileEmpty)(); + +@end + +#endif /* Lottie_h */ diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m new file mode 100755 index 0000000000..3f87ea0066 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m @@ -0,0 +1,100 @@ +#import + +@implementation ChatInputTextViewImplTargetForAction + +- (instancetype)initWithTarget:(id _Nullable)target { + self = [super init]; + if (self != nil) { + _target = target; + } + return self; +} + +@end + +@implementation ChatInputTextViewImpl + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + if (_targetForActionImpl) { + ChatInputTextViewImplTargetForAction *result = _targetForActionImpl(action); + if (result) { + return result.target != nil; + } + } + + if (action == @selector(paste:)) { + NSArray *items = [UIMenuController sharedMenuController].menuItems; + if (((UIMenuItem *)items.firstObject).action == @selector(toggleBoldface:)) { + return false; + } + return true; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + static SEL promptForReplaceSelector; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + promptForReplaceSelector = NSSelectorFromString(@"_promptForReplace:"); + }); + if (action == promptForReplaceSelector) { + return false; + } +#pragma clang diagnostic pop + + if (action == @selector(toggleUnderline:)) { + return false; + } + + return [super canPerformAction:action withSender:sender]; +} + +- (id)targetForAction:(SEL)action withSender:(id)__unused sender +{ + if (_targetForActionImpl) { + ChatInputTextViewImplTargetForAction *result = _targetForActionImpl(action); + if (result) { + return result.target; + } + } + return [super targetForAction:action withSender:sender]; +} + +- (void)copy:(id)sender { + if (_shouldCopy == nil || _shouldCopy()) { + [super copy:sender]; + } +} + +- (void)paste:(id)sender +{ + if (_shouldPaste == nil || _shouldPaste()) { + [super paste:sender]; + } +} + +- (NSArray *)keyCommands { + UIKeyCommand *plainReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:kNilOptions action:@selector(handlePlainReturn:)]; + return @[ + plainReturn + ]; +} + +- (void)handlePlainReturn:(id)__unused sender { + if (_shouldReturn) { + _shouldReturn(); + } +} + +- (void)deleteBackward { + bool notify = self.text.length == 0; + [super deleteBackward]; + if (notify) { + if (_backspaceWhileEmpty) { + _backspaceWhileEmpty(); + } + } +} + +@end diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift new file mode 100644 index 0000000000..8faa600c58 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift @@ -0,0 +1,691 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import AppBundle +import ChatInputTextViewImpl + +public protocol ChatInputTextNodeDelegate: AnyObject { + func chatInputTextNodeDidUpdateText() + func chatInputTextNodeShouldReturn() -> Bool + func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) + func chatInputTextNodeDidBeginEditing() + func chatInputTextNodeDidFinishEditing() + func chatInputTextNodeBackspaceWhileEmpty() + + @available(iOS 13.0, *) + func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu + + func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool + func chatInputTextNodeShouldCopy() -> Bool + func chatInputTextNodeShouldPaste() -> Bool +} + +open class ChatInputTextNode: ASDisplayNode, UITextViewDelegate { + public weak var delegate: ChatInputTextNodeDelegate? { + didSet { + self.textView.customDelegate = self.delegate + } + } + + private var selectionChangedForEditedText: Bool = false + private var isPreservingSelection: Bool = false + + public var textView: ChatInputTextView { + return self.view as! ChatInputTextView + } + + public var keyboardAppearance: UIKeyboardAppearance { + get { + return self.textView.keyboardAppearance + } + set { + guard newValue != self.keyboardAppearance else { + return + } + self.textView.keyboardAppearance = newValue + self.textView.reloadInputViews() + } + } + + public var initialPrimaryLanguage: String? { + get { + return self.textView.initialPrimaryLanguage + } set(value) { + self.textView.initialPrimaryLanguage = value + } + } + + public func isCurrentlyEmoji() -> Bool { + return false + } + + public var textInputMode: UITextInputMode? { + return self.textView.textInputMode + } + + public var selectedRange: NSRange { + get { + return self.textView.selectedRange + } set(value) { + if self.textView.selectedRange != value { + self.textView.selectedRange = value + } + } + } + + public var attributedText: NSAttributedString? { + 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() + } + } + } + + public var isRTL: Bool { + return self.textView.isRTL + } + + public var selectionRect: CGRect { + guard let range = self.textView.selectedTextRange else { + return self.textView.bounds + } + return self.textView.firstRect(for: range) + } + + public var textContainerInset: UIEdgeInsets { + get { + return self.textView.defaultTextContainerInset + } set(value) { + let targetValue = UIEdgeInsets(top: value.top, left: 0.0, bottom: value.bottom, right: 0.0) + if self.textView.defaultTextContainerInset != value { + self.textView.defaultTextContainerInset = targetValue + } + } + } + + override public init() { + super.init() + + self.setViewBlock({ + return ChatInputTextView() + }) + + self.textView.delegate = self + } + + public func resetInitialPrimaryLanguage() { + } + + public func textHeightForWidth(_ width: CGFloat) -> CGFloat { + return self.textView.textHeightForWidth(width) + } + + @objc public func textViewDidBeginEditing(_ textView: UITextView) { + self.delegate?.chatInputTextNodeDidBeginEditing() + } + + @objc public func textViewDidEndEditing(_ textView: UITextView) { + self.delegate?.chatInputTextNodeDidFinishEditing() + } + + @objc public func textViewDidChange(_ textView: UITextView) { + self.selectionChangedForEditedText = true + + self.delegate?.chatInputTextNodeDidUpdateText() + + self.textView.updateTextContainerInset() + } + + @objc public func textViewDidChangeSelection(_ textView: UITextView) { + if self.isPreservingSelection { + return + } + + self.selectionChangedForEditedText = false + + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.delegate?.chatInputTextNodeDidChangeSelection(dueToEditing: self.selectionChangedForEditedText) + } + } + + @available(iOS 16.0, *) + @objc public func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? { + return self.delegate?.chatInputTextNodeMenu(forTextRange: range, suggestedActions: suggestedActions) + } + + @objc public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + guard let delegate = self.delegate else { + return true + } + return delegate.chatInputTextNode(shouldChangeTextIn: range, replacementText: text) + } + + public func updateLayout(size: CGSize) { + self.textView.updateLayout(size: size) + } +} + +private final class ChatInputTextContainer: NSTextContainer { + override var isSimpleRectangularTextContainer: Bool { + return false + } + + override init(size: CGSize) { + super.init(size: size) + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func lineFragmentRect(forProposedRect proposedRect: CGRect, at characterIndex: Int, writingDirection baseWritingDirection: NSWritingDirection, remaining remainingRect: UnsafeMutablePointer?) -> CGRect { + var result = super.lineFragmentRect(forProposedRect: proposedRect, at: characterIndex, writingDirection: baseWritingDirection, remaining: remainingRect) + + result.origin.x -= 5.0 + result.size.width -= 5.0 + + if let textStorage = self.layoutManager?.textStorage { + let string: NSString = textStorage.string as NSString + let index = Int(characterIndex) + if index >= 0 && index < string.length { + let attributes = textStorage.attributes(at: index, effectiveRange: nil) + let blockQuote = attributes[NSAttributedString.Key(rawValue: "Attribute__Blockquote")] as? NSObject + if let blockQuote { + result.origin.x += 9.0 + result.size.width -= 9.0 + result.size.width -= 7.0 + + var isFirstLine = false + if index == 0 { + isFirstLine = true + } else { + let previousAttributes = textStorage.attributes(at: index - 1, effectiveRange: nil) + let previousBlockQuote = previousAttributes[NSAttributedString.Key(rawValue: "Attribute__Blockquote")] as? NSObject + if let previousBlockQuote { + if !blockQuote.isEqual(previousBlockQuote) { + isFirstLine = true + } + } else { + isFirstLine = true + } + } + + if (isFirstLine) { + result.size.width -= 18.0 + } + } + } + } + + return result + } +} + +public final class ChatInputTextView: ChatInputTextViewImpl, NSLayoutManagerDelegate, NSTextStorageDelegate { + public final class Theme: Equatable { + public final class Quote: Equatable { + public let background: UIColor + public let foreground: UIColor + + public init( + background: UIColor, + foreground: UIColor + ) { + self.background = background + self.foreground = foreground + } + + public static func ==(lhs: Quote, rhs: Quote) -> Bool { + if !lhs.background.isEqual(rhs.background) { + return false + } + if !lhs.foreground.isEqual(rhs.foreground) { + return false + } + return true + } + } + + public let quote: Quote + + public init(quote: Quote) { + self.quote = quote + } + + public static func ==(lhs: Theme, rhs: Theme) -> Bool { + if lhs.quote != rhs.quote { + return false + } + return true + } + } + + public weak var customDelegate: ChatInputTextNodeDelegate? + + public var theme: Theme? { + didSet { + if self.theme != oldValue { + self.updateTextElements() + } + } + } + + private let customTextContainer: ChatInputTextContainer + private let customTextStorage: NSTextStorage + private let customLayoutManager: NSLayoutManager + + private let measurementTextContainer: ChatInputTextContainer + private let measurementTextStorage: NSTextStorage + private let measurementLayoutManager: NSLayoutManager + + private var blockQuotes: [Int: QuoteBackgroundView] = [:] + + public var defaultTextContainerInset: UIEdgeInsets = UIEdgeInsets() { + didSet { + if self.defaultTextContainerInset != oldValue { + self.updateTextContainerInset() + } + } + } + + private var didInitializePrimaryInputLanguage: Bool = false + public var initialPrimaryLanguage: String? + + override public var textInputMode: UITextInputMode? { + if !self.didInitializePrimaryInputLanguage { + self.didInitializePrimaryInputLanguage = true + if let initialPrimaryLanguage = self.initialPrimaryLanguage { + for inputMode in UITextInputMode.activeInputModes { + if let primaryLanguage = inputMode.primaryLanguage, primaryLanguage == initialPrimaryLanguage { + return inputMode + } + } + } + } + return super.textInputMode + } + + public init() { + self.customTextContainer = ChatInputTextContainer(size: CGSize(width: 100.0, height: 100000.0)) + self.customLayoutManager = NSLayoutManager() + self.customTextStorage = NSTextStorage() + self.customTextStorage.addLayoutManager(self.customLayoutManager) + self.customLayoutManager.addTextContainer(self.customTextContainer) + + self.measurementTextContainer = ChatInputTextContainer(size: CGSize(width: 100.0, height: 100000.0)) + self.measurementLayoutManager = NSLayoutManager() + self.measurementTextStorage = NSTextStorage() + self.measurementTextStorage.addLayoutManager(self.measurementLayoutManager) + self.measurementLayoutManager.addTextContainer(self.measurementTextContainer) + + super.init(frame: CGRect(), textContainer: self.customTextContainer) + + self.textContainerInset = UIEdgeInsets() + self.backgroundColor = nil + self.isOpaque = false + + self.customTextContainer.widthTracksTextView = false + self.customTextContainer.heightTracksTextView = false + + self.measurementTextContainer.widthTracksTextView = false + self.measurementTextContainer.heightTracksTextView = false + + self.customLayoutManager.delegate = self + self.measurementLayoutManager.delegate = self + + self.customTextStorage.delegate = self + self.measurementTextStorage.delegate = self + + self.shouldCopy = { [weak self] in + guard let self else { + return true + } + return self.customDelegate?.chatInputTextNodeShouldCopy() ?? true + } + self.shouldPaste = { [weak self] in + guard let self else { + return true + } + return self.customDelegate?.chatInputTextNodeShouldPaste() ?? true + } + self.shouldReturn = { [weak self] in + guard let self else { + return true + } + return self.customDelegate?.chatInputTextNodeShouldReturn() ?? true + } + self.backspaceWhileEmpty = { [weak self] in + guard let self else { + return + } + self.customDelegate?.chatInputTextNodeBackspaceWhileEmpty() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc public func layoutManager(_ layoutManager: NSLayoutManager, paragraphSpacingBeforeGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat { + guard let textStorage = layoutManager.textStorage else { + return 0.0 + } + let characterIndex = Int(layoutManager.characterIndexForGlyph(at: glyphIndex)) + if characterIndex < 0 || characterIndex >= textStorage.length { + return 0.0 + } + + let attributes = textStorage.attributes(at: characterIndex, effectiveRange: nil) + guard let blockQuote = attributes[NSAttributedString.Key("Attribute__Blockquote")] as? NSObject else { + return 0.0 + } + + if characterIndex != 0 { + let previousAttributes = textStorage.attributes(at: characterIndex - 1, effectiveRange: nil) + let previousBlockQuote = previousAttributes[NSAttributedString.Key("Attribute__Blockquote")] as? NSObject + if let previousBlockQuote, blockQuote.isEqual(previousBlockQuote) { + return 0.0 + } + } + + return 8.0 + } + + @objc public func layoutManager(_ layoutManager: NSLayoutManager, paragraphSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat { + guard let textStorage = layoutManager.textStorage else { + return 0.0 + } + var characterIndex = Int(layoutManager.characterIndexForGlyph(at: glyphIndex)) + characterIndex -= 1 + if characterIndex < 0 { + characterIndex = 0 + } + if characterIndex < 0 || characterIndex >= textStorage.length { + return 0.0 + } + + let attributes = textStorage.attributes(at: characterIndex, effectiveRange: nil) + guard let blockQuote = attributes[NSAttributedString.Key("Attribute__Blockquote")] as? NSObject else { + return 0.0 + } + + if characterIndex + 1 < textStorage.length { + let nextAttributes = textStorage.attributes(at: characterIndex + 1, effectiveRange: nil) + let nextBlockQuote = nextAttributes[NSAttributedString.Key("Attribute__Blockquote")] as? NSObject + if let nextBlockQuote, blockQuote.isEqual(nextBlockQuote) { + return 0.0 + } + } + + return 8.0 + } + + public func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) { + if textStorage !== self.customTextStorage { + return + } + } + + public func layoutManager(_ layoutManager: NSLayoutManager, didCompleteLayoutFor textContainer: NSTextContainer?, atEnd layoutFinishedFlag: Bool) { + if textContainer !== self.customTextContainer { + return + } + self.updateTextElements() + } + + public func updateTextContainerInset() { + var result = self.defaultTextContainerInset + + if self.customTextStorage.length != 0 { + let topAttributes = self.customTextStorage.attributes(at: 0, effectiveRange: nil) + let bottomAttributes = self.customTextStorage.attributes(at: self.customTextStorage.length - 1, effectiveRange: nil) + + if topAttributes[NSAttributedString.Key("Attribute__Blockquote")] != nil { + result.top += 7.0 + } + if bottomAttributes[NSAttributedString.Key("Attribute__Blockquote")] != nil { + result.bottom += 8.0 + } + } + + if self.textContainerInset != result { + self.textContainerInset = result + } + self.updateTextElements() + } + + public func textHeightForWidth(_ width: CGFloat) -> CGFloat { + let measureSize = CGSize(width: width, height: 1000000.0) + + if self.measurementTextStorage != self.attributedText || self.measurementTextContainer.size != measureSize { + self.measurementTextStorage.setAttributedString(self.attributedText) + self.measurementTextContainer.size = measureSize + self.measurementLayoutManager.invalidateLayout(forCharacterRange: NSRange(location: 0, length: self.measurementTextStorage.length), actualCharacterRange: nil) + self.measurementLayoutManager.ensureLayout(for: self.measurementTextContainer) + } + + let textSize = self.measurementLayoutManager.usedRect(for: self.measurementTextContainer).size + + return textSize.height + self.textContainerInset.top + self.textContainerInset.bottom + } + + public func updateLayout(size: CGSize) { + let measureSize = CGSize(width: size.width, height: 1000000.0) + + if self.textContainer.size != measureSize { + self.textContainer.size = measureSize + self.customLayoutManager.invalidateLayout(forCharacterRange: NSRange(location: 0, length: self.customTextStorage.length), actualCharacterRange: nil) + self.customLayoutManager.ensureLayout(for: self.customTextContainer) + } + } + + override public func layoutSubviews() { + super.layoutSubviews() + } + + public func updateTextElements() { + var blockQuoteIndex = 0 + var validBlockQuotes: [Int] = [] + + self.textStorage.enumerateAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), in: NSRange(location: 0, length: self.textStorage.length), using: { value, range, _ in + if let value { + let _ = value + + 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 + + let blockQuote: QuoteBackgroundView + if let current = self.blockQuotes[id] { + blockQuote = current + } else { + blockQuote = QuoteBackgroundView() + self.blockQuotes[id] = blockQuote + self.insertSubview(blockQuote, at: 0) + } + + var boundingRect = self.customLayoutManager.boundingRect(forGlyphRange: glyphRange, in: self.customTextContainer) + + 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 -= 9.0 + boundingRect.size.width += 9.0 + boundingRect.size.width += 18.0 + boundingRect.size.width = min(boundingRect.size.width, self.bounds.width - 18.0) + + boundingRect.origin.y -= 4.0 + boundingRect.size.height += 8.0 + + blockQuote.frame = boundingRect + if let theme = self.theme { + blockQuote.update(size: boundingRect.size, theme: theme.quote) + } + + validBlockQuotes.append(blockQuoteIndex) + blockQuoteIndex += 1 + } + }) + + var removedBlockQuotes: [Int] = [] + for (id, blockQuote) in self.blockQuotes { + if !validBlockQuotes.contains(id) { + removedBlockQuotes.append(id) + blockQuote.removeFromSuperview() + } + } + for id in removedBlockQuotes { + self.blockQuotes.removeValue(forKey: id) + } + } + + override public func caretRect(for position: UITextPosition) -> CGRect { + var result = super.caretRect(for: position) + guard let textStorage = self.customLayoutManager.textStorage else { + return result + } + let _ = textStorage + + let index = self.offset(from: self.beginningOfDocument, to: position) + + let glyphRange = self.customLayoutManager.glyphRange(forCharacterRange: NSMakeRange(index, 1), actualCharacterRange: nil) + var boundingRect = self.customLayoutManager.boundingRect(forGlyphRange: glyphRange, in: self.customTextContainer) + + boundingRect.origin.y += 5.0 + + result.origin.y = boundingRect.minY + result.size.height = boundingRect.height + + return result + } + + override public func selectionRects(for range: UITextRange) -> [UITextSelectionRect] { + let sourceRects = super.selectionRects(for: range) + + var result: [UITextSelectionRect] = [] + for rect in sourceRects { + var mappedRect = rect.rect + //mappedRect.size.height = 10.0 + mappedRect.size.height += 0.0 + result.append(CustomTextSelectionRect( + rect: mappedRect, + writingDirection: rect.writingDirection, + containsStart: rect.containsStart, + containsEnd: rect.containsEnd, + isVertical: rect.isVertical + )) + } + + return result + } +} + +private final class CustomTextSelectionRect: UITextSelectionRect { + let rectValue: CGRect + let writingDirectionValue: NSWritingDirection + let containsStartValue: Bool + let containsEndValue: Bool + let isVerticalValue: Bool + + override var rect: CGRect { + return self.rectValue + } + override var writingDirection: NSWritingDirection { + return self.writingDirectionValue + } + override var containsStart: Bool { + return self.containsStartValue + } + override var containsEnd: Bool { + return self.containsEndValue + } + override var isVertical: Bool { + return self.isVerticalValue + } + + init(rect: CGRect, writingDirection: NSWritingDirection, containsStart: Bool, containsEnd: Bool, isVertical: Bool) { + self.rectValue = rect + self.writingDirectionValue = writingDirection + self.containsStartValue = containsStart + self.containsEndValue = containsEnd + self.isVerticalValue = isVertical + } +} + +private let quoteIcon: UIImage = { + return UIImage(bundleImageName: "Chat/Message/ReplyQuoteIcon")!.precomposed().withRenderingMode(.alwaysTemplate) +}() + +private final class QuoteBackgroundView: UIView { + private let lineLayer: SimpleLayer + private let iconView: UIImageView + + private var theme: ChatInputTextView.Theme.Quote? + + override init(frame: CGRect) { + self.lineLayer = SimpleLayer() + self.iconView = UIImageView(image: quoteIcon) + + super.init(frame: frame) + + self.layer.addSublayer(self.lineLayer) + self.addSubview(self.iconView) + + self.layer.cornerRadius = 3.0 + self.clipsToBounds = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(size: CGSize, theme: ChatInputTextView.Theme.Quote) { + if self.theme != theme { + self.theme = theme + + self.backgroundColor = theme.background + self.lineLayer.backgroundColor = theme.foreground.cgColor + self.iconView.tintColor = theme.foreground + } + + self.lineLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 00), size: CGSize(width: 3.0, height: size.height)) + self.iconView.frame = CGRect(origin: CGPoint(x: size.width - 4.0 - quoteIcon.size.width, y: 4.0), size: quoteIcon.size) + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift index 6e6de8ccbf..ecd3d1fc34 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift @@ -133,8 +133,8 @@ public struct ChatMessageItemLayoutConstants { } public static var compact: ChatMessageItemLayoutConstants { - let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) - let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0)) + let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 3.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 5.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) + let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 11.0, bottom: 6.0 - UIScreenPixel, right: 11.0)) let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 0.0, maxDimensions: CGSize(width: 300.0, height: 380.0), minDimensions: CGSize(width: 170.0, height: 74.0)) let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0) let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) @@ -145,8 +145,8 @@ public struct ChatMessageItemLayoutConstants { } public static var regular: ChatMessageItemLayoutConstants { - let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.65), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) - let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0)) + let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 3.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.65), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 5.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) + let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 10.0, bottom: 6.0 - UIScreenPixel, right: 10.0)) let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 440.0, height: 440.0), minDimensions: CGSize(width: 170.0, height: 74.0)) let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0) let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/BUILD new file mode 100644 index 0000000000..34a62a8140 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageReplyInfoNode", + module_name = "ChatMessageReplyInfoNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/TelegramPresentationData", + "//submodules/ChatPresentationInterfaceState", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/AccessoryPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/AccessoryPanelNode.swift new file mode 100644 index 0000000000..17ff62255f --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/AccessoryPanelNode.swift @@ -0,0 +1,23 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramPresentationData +import ChatPresentationInterfaceState + +open class AccessoryPanelNode: ASDisplayNode { + open var originalFrameBeforeDismissed: CGRect? + open var dismiss: (() -> Void)? + open var interfaceInteraction: ChatPanelInterfaceInteraction? + + open func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + } + + open func updateState(size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState) { + } + + open func animateIn() { + } + + open func animateOut() { + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index a39f6a6b9f..b715dda2d6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -344,7 +344,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let textFont = item.presentationData.messageFont if let entities = entities { - attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseQuoteTintColor: messageTheme.accentControlColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, message: item.message) + attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseQuoteTintColor: messageTheme.accentControlColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, message: item.message, adjustQuoteFontSize: true) } else if !rawText.isEmpty { attributedText = NSAttributedString(string: rawText, font: textFont, textColor: messageTheme.primaryTextColor) } else { @@ -789,7 +789,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { guard let strongSelf = self, let item = strongSelf.item else { return } - item.controllerInteraction.performTextSelectionAction(true, text, action) + item.controllerInteraction.performTextSelectionAction(item.message, true, text, action) }) textSelectionNode.updateRange = { [weak self] selectionRange in if let strongSelf = self, let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange { @@ -801,6 +801,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } } + textSelectionNode.enableQuote = item.controllerInteraction.canSetupReply(item.message) == .reply self.textSelectionNode = textSelectionNode self.addSubnode(textSelectionNode) self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode.textNode) diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 9fc7411629..12d3a84d17 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -139,7 +139,7 @@ public final class ChatControllerInteraction { public let scheduleCurrentMessage: () -> Void public let sendScheduledMessagesNow: ([MessageId]) -> Void public let editScheduledMessagesTime: ([MessageId]) -> Void - public let performTextSelectionAction: (Bool, NSAttributedString, TextSelectionAction) -> Void + public let performTextSelectionAction: (Message?, Bool, NSAttributedString, TextSelectionAction) -> Void public let displayImportedMessageTooltip: (ASDisplayNode) -> Void public let displaySwipeToReplyHint: () -> Void public let dismissReplyMarkupMessage: (Message) -> Void @@ -253,7 +253,7 @@ public final class ChatControllerInteraction { scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, - performTextSelectionAction: @escaping (Bool, NSAttributedString, TextSelectionAction) -> Void, + performTextSelectionAction: @escaping (Message?, Bool, NSAttributedString, TextSelectionAction) -> Void, displayImportedMessageTooltip: @escaping (ASDisplayNode) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 1ab0330a14..01a00da45e 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -682,7 +682,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { hasEntityKeyboard = true } - let controller = ChatSendMessageActionSheetController(context: strongSelf.context, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, canSendWhenOnline: false, completion: { + let controller = ChatSendMessageActionSheetController(context: strongSelf.context, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputView: textInputNode.textView, canSendWhenOnline: false, completion: { }, sendMessage: { [weak textInputPanelNode] mode in switch mode { case .generic: diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index eb95273096..ce8c2cabf3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -540,7 +540,8 @@ final class StoryContentCaptionComponent: Component { fixedFont: Font.monospace(16.0), blockQuoteFont: Font.monospace(16.0), message: nil, - entityFiles: component.entityFiles + entityFiles: component.entityFiles, + adjustQuoteFontSize: true ) let truncationToken = NSMutableAttributedString() diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index abc00ced54..0961a2b867 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -4140,6 +4140,8 @@ public final class StoryItemSetContainerComponent: Component { } case .translate: self.sendMessageContext.performTranslateTextAction(view: self, text: text.string) + case .quote: + break } }, controller: { [weak self] in diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 4e741492ec..ea103b1f1e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -1810,7 +1810,7 @@ final class StoryItemSetContainerSendMessage { let _ = self /*if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() }*/ @@ -2153,7 +2153,7 @@ final class StoryItemSetContainerSendMessage { } let file = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, replyToStoryId: replyToStoryId, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: replyToStoryId, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) messages.append(message) } if let _ = groupingKey, messages.count % 10 == 0 { @@ -2243,7 +2243,7 @@ final class StoryItemSetContainerSendMessage { guard let self, let view, let component = view.component else { return } - if component.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peer.id, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyMessageId, replyToStoryId: storyId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime) { + if component.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peer.id, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: storyId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime) { } if let attachmentController = self.attachmentController { @@ -2599,7 +2599,7 @@ final class StoryItemSetContainerSendMessage { mappedMessages.append(message) } - strongSelf.sendMessages(view: view, peer: peer, messages: mappedMessages.map { $0.withUpdatedReplyToMessageId(replyToMessageId).withUpdatedReplyToStoryId(replyToStoryId) }, silentPosting: silentPosting, scheduleTime: scheduleTime) + strongSelf.sendMessages(view: view, peer: peer, messages: mappedMessages.map { $0.withUpdatedReplyToMessageId(replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }).withUpdatedReplyToStoryId(replyToStoryId) }, silentPosting: silentPosting, scheduleTime: scheduleTime) completion() } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 5a5d19e4bf..cce7c9a3e2 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2483,7 +2483,7 @@ private func extractAccountManagerState(records: AccountRecordsView map { messageIds -> MessageId? in if messageIds.isEmpty { return nil diff --git a/submodules/TelegramUI/Sources/ChatBotInfoItem.swift b/submodules/TelegramUI/Sources/ChatBotInfoItem.swift index 9f2f23562a..e2198870ae 100644 --- a/submodules/TelegramUI/Sources/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/Sources/ChatBotInfoItem.swift @@ -230,7 +230,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { updatedTextAndEntities = (item.text, generateTextEntities(item.text, enabledTypes: .all)) } - let attributedText = stringWithAppliedEntities(updatedTextAndEntities.0, entities: updatedTextAndEntities.1, baseColor: item.presentationData.theme.theme.chat.message.infoPrimaryTextColor, linkColor: item.presentationData.theme.theme.chat.message.infoLinkTextColor, baseFont: messageFont, linkFont: messageFont, boldFont: messageBoldFont, italicFont: messageItalicFont, boldItalicFont: messageBoldItalicFont, fixedFont: messageFixedFont, blockQuoteFont: messageFont, message: nil) + let attributedText = stringWithAppliedEntities(updatedTextAndEntities.0, entities: updatedTextAndEntities.1, baseColor: item.presentationData.theme.theme.chat.message.infoPrimaryTextColor, linkColor: item.presentationData.theme.theme.chat.message.infoLinkTextColor, baseFont: messageFont, linkFont: messageFont, boldFont: messageBoldFont, italicFont: messageItalicFont, boldItalicFont: messageBoldItalicFont, fixedFont: messageFixedFont, blockQuoteFont: messageFont, message: nil, adjustQuoteFontSize: true) let horizontalEdgeInset: CGFloat = 10.0 + params.leftInset let horizontalContentInset: CGFloat = 12.0 diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 773433e953..36d193ceed 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2062,7 +2062,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -2074,9 +2074,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let peerId = strongSelf.chatLocation.peerId if peerId?.namespace != Namespaces.Peer.SecretChat, let interactiveEmojis = strongSelf.chatDisplayNode.interactiveEmojis, interactiveEmojis.emojis.contains(text) { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: text)), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: text)), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } else { - strongSelf.sendMessages([.message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } }, sendSticker: { [weak self] fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets in guard let strongSelf = self else { @@ -2113,7 +2113,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var current = current current = current.updatedInterfaceState { interfaceState in var interfaceState = interfaceState - interfaceState = interfaceState.withUpdatedReplyMessageId(nil) + interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil) if clearInput { interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) } @@ -2172,7 +2172,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)] + let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)] if silentPosting { let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting) strongSelf.sendMessages(transformedMessages) @@ -2238,7 +2238,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }.updatedInputMode { current in + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }.updatedInputMode { current in if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { return .media(mode: mode, expanded: nil, focused: focused) } @@ -2248,7 +2248,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, nil) - var messages = [EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + var messages = [EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] if silentPosting { messages = strongSelf.transformEnqueueMessages(messages, silentPosting: true) strongSelf.sendMessages(messages) @@ -2664,7 +2664,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } }) } }, nil) @@ -2673,7 +2673,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - strongSelf.sendMessages([.message(text: command, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + var replyToMessageId: EngineMessageReplySubject? + if postAsReply, let messageId { + replyToMessageId = EngineMessageReplySubject(messageId: messageId, quote: nil) + } + strongSelf.sendMessages([.message(text: command, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.effectiveNavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) { @@ -3637,7 +3641,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.sendCurrentMessage(scheduleTime: time) { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: strongSelf.presentationInterfaceState.subject != .scheduledMessages, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } }) if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { @@ -3687,7 +3691,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }, delay: true) } - }, performTextSelectionAction: { [weak self] canCopy, text, action in + }, performTextSelectionAction: { [weak self] message, canCopy, text, action in guard let strongSelf = self else { return } @@ -3769,7 +3773,40 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { f() } + case let .quote(range): + if let currentContextController = strongSelf.currentContextController { + currentContextController.dismiss(completion: { + }) + } + let completion: (ContainedViewLayoutTransition) -> Void = { _ in } + if let messageId = message?.id { + if canSendMessagesToChat(strongSelf.presentationInterfaceState) { + let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { + if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + var quoteData: EngineMessageReplyQuote? + + let quoteText = (message.text as NSString).substring(with: NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound)) + quoteData = EngineMessageReplyQuote(text: quoteText, entities: []) + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( + messageId: message.id, + quote: quoteData + )) }).updatedSearch(nil).updatedShowCommands(false) }, completion: completion) + strongSelf.updateItemNodesSearchTextHighlightStates() + strongSelf.chatDisplayNode.ensureInputViewFocused() + } else { + completion(.immediate) + } + }, alertAction: { + completion(.immediate) + }, delay: true) + } else { + completion(.immediate) + } + } else { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil) }) }, completion: completion) + } } }, displayImportedMessageTooltip: { [weak self] _ in guard let strongSelf = self else { @@ -8337,7 +8374,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if canSendMessagesToChat(strongSelf.presentationInterfaceState) { let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageId(message.id) }).updatedSearch(nil).updatedShowCommands(false) }, completion: completion) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ + $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( + messageId: message.id, + quote: nil + )) + }).updatedSearch(nil).updatedShowCommands(false) }, completion: completion) strongSelf.updateItemNodesSearchTextHighlightStates() strongSelf.chatDisplayNode.ensureInputViewFocused() } else { @@ -8350,7 +8392,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G completion(.immediate) } } else { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageId(nil) }) }, completion: completion) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil) }) }, completion: completion) } }, setupEditMessage: { [weak self] messageId, completion in if let strongSelf = self, strongSelf.isNodeLoaded { @@ -9169,13 +9211,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { messageText = command } - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } }) } }, nil) @@ -9184,7 +9226,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - strongSelf.sendMessages([.message(text: messageText, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: messageText, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) strongSelf.interfaceInteraction?.updateShowCommands { _ in return false } @@ -10371,7 +10413,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.window?.presentInGlobalOverlay(slowmodeTooltipController) }, displaySendMessageOptions: { [weak self] node, gesture in - if let strongSelf = self, let peerId = strongSelf.chatLocation.peerId, let textInputNode = strongSelf.chatDisplayNode.textInputNode(), let layout = strongSelf.validLayout { + if let strongSelf = self, let peerId = strongSelf.chatLocation.peerId, let textInputView = strongSelf.chatDisplayNode.textInputView(), let layout = strongSelf.validLayout { let previousSupportedOrientations = strongSelf.supportedOrientations if layout.size.width > layout.size.height { strongSelf.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .landscape) @@ -10407,7 +10449,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = ApplicationSpecificNotice.incrementSendWhenOnlineTip(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).startStandalone() } - let controller = ChatSendMessageActionSheetController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, canSendWhenOnline: sendWhenOnlineAvailable, completion: { [weak self] in + let controller = ChatSendMessageActionSheetController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputView: textInputView, canSendWhenOnline: sendWhenOnlineAvailable, completion: { [weak self] in if let strongSelf = self { strongSelf.supportedOrientations = previousSupportedOrientations } @@ -10422,7 +10464,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.sendCurrentMessage(scheduleTime: scheduleWhenOnlineTimestamp) { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: strongSelf.presentationInterfaceState.subject != .scheduledMessages, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } }) strongSelf.openScheduledMessages() } @@ -11852,7 +11894,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } interfaceState = interfaceState.withUpdatedInputLanguage(self.chatDisplayNode.currentTextInputLanguage) if case .peer = self.chatLocation, let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) { - interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState()).withUpdatedReplyMessageId(nil) + interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState()).withUpdatedReplyMessageSubject(nil) } let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: threadId, { _ in return interfaceState @@ -11978,8 +12020,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup { - if temporaryChatPresentationInterfaceState.interfaceState.replyMessageId == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { - temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageId(keyboardButtonsMessage.id).withUpdatedMessageActionsState({ value in + if temporaryChatPresentationInterfaceState.interfaceState.replyMessageSubject == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { + temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ + $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( + messageId: keyboardButtonsMessage.id, + quote: nil + )).withUpdatedMessageActionsState({ value in var value = value value.processedSetupReplyMessageId = keyboardButtonsMessage.id return value @@ -11998,8 +12044,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, keyboardButtonsMessage.requestsSetupReply { - if temporaryChatPresentationInterfaceState.interfaceState.replyMessageId == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { - temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageId(keyboardButtonsMessage.id).withUpdatedMessageActionsState({ value in + if temporaryChatPresentationInterfaceState.interfaceState.replyMessageSubject == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { + temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( + messageId: keyboardButtonsMessage.id, + quote: nil + )).withUpdatedMessageActionsState({ value in var value = value value.processedSetupReplyMessageId = keyboardButtonsMessage.id return value @@ -13627,14 +13676,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -13692,17 +13741,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let media = media { - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) enqueueMessages.append(message) } } @@ -13753,13 +13802,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if contactData.isPrimitive { let phone = contactData.basicData.phoneNumbers[0].value let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -13768,7 +13817,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let textEnqueueMessage = textEnqueueMessage { enqueueMessages.append(textEnqueueMessage) } - enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) } else { let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in @@ -13778,13 +13827,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let phone = contactData.basicData.phoneNumbers[0].value if let vCardData = contactData.serializedVCard() { let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -13793,7 +13842,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let textEnqueueMessage = textEnqueueMessage { enqueueMessages.append(textEnqueueMessage) } - enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) } }), completed: nil, cancelled: nil) @@ -13834,8 +13883,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G fromAttachMenu = false } let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageId, threadId: strongSelf.chatLocation.threadId) + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId) controller.openUrl = { [weak self] url, concealed, commit in self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) } @@ -13845,7 +13894,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G controller.completion = { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() } @@ -14217,7 +14266,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.enqueueMediaMessageDisposable.set((combineLatest(signals) |> deliverOnMainQueue).startStrict(next: { results in if let strongSelf = self { - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject for item in results { if let item = item { @@ -14277,7 +14326,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) messages.append(message) } if let _ = groupingKey, messages.count % 10 == 0 { @@ -14294,7 +14343,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -14643,14 +14692,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -14693,17 +14742,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let media = media { - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) enqueueMessages.append(message) } } @@ -14754,17 +14803,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if contactData.isPrimitive { let phone = contactData.basicData.phoneNumbers[0].value let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) strongSelf.sendMessages([message]) } else { let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in @@ -14774,17 +14823,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let phone = contactData.basicData.phoneNumbers[0].value if let vCardData = contactData.serializedVCard() { let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) strongSelf.sendMessages([message]) } }), completed: nil, cancelled: nil) @@ -15113,13 +15162,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) @@ -15144,7 +15193,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G correlationId: nil, bubbleUpEmojiOrStickersets: [] ) - strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageId)]) + strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)]) }) } @@ -15314,12 +15363,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private func transformEnqueueMessages(_ messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil) -> [EnqueueMessage] { - var defaultReplyMessageId: MessageId? + var defaultReplyMessageSubject: EngineMessageReplySubject? switch self.chatLocation { case .peer: break case let .replyThread(replyThreadMessage): - defaultReplyMessageId = replyThreadMessage.messageId + defaultReplyMessageSubject = EngineMessageReplySubject(messageId: replyThreadMessage.messageId, quote: nil) case .feed: break } @@ -15327,11 +15376,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return messages.map { message in var message = message - if let defaultReplyMessageId = defaultReplyMessageId { + if let defaultReplyMessageSubject = defaultReplyMessageSubject { switch message { case let .message(text, attributes, inlineStickers, mediaReference, replyToMessageId, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): if replyToMessageId == nil { - message = .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, replyToMessageId: defaultReplyMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) + message = .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, replyToMessageId: defaultReplyMessageSubject, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) } case .forward: break @@ -15491,19 +15540,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } completionImpl?() }, usedCorrelationId) - strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageId) }, media: true) + strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }, media: true) if let _ = scheduleTime { completion() @@ -15543,17 +15592,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private func enqueueGifData(_ data: Data) { self.enqueueMediaMessageDisposable.set((legacyEnqueueGifMessage(account: self.context.account, data: data) |> deliverOnMainQueue).startStrict(next: { [weak self] message in if let strongSelf = self { - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) }) + strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }) } })) } @@ -15561,17 +15610,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private func enqueueVideoData(_ data: Data) { self.enqueueMediaMessageDisposable.set((legacyEnqueueGifMessage(account: self.context.account, data: data) |> deliverOnMainQueue).startStrict(next: { [weak self] message in if let strongSelf = self { - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) }) + strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }) } })) } @@ -15591,17 +15640,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes) let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) }) + strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }) } })) } @@ -15624,8 +15673,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let self else { return } - let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId - if self.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peerId, threadId: self.chatLocation.threadId, botId: results.botId, result: result, replyToMessageId: replyMessageId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime) { + let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject + if self.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peerId, threadId: self.chatLocation.threadId, botId: results.botId, result: result, replyToMessageId: replyMessageSubject?.subjectModel, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime) { self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() @@ -15635,7 +15684,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if resetTextInputState { state = state.updatedInterfaceState { interfaceState in var interfaceState = interfaceState - interfaceState = interfaceState.withUpdatedReplyMessageId(nil) + interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil) interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) interfaceState = interfaceState.withUpdatedComposeDisableUrlPreview(nil) return interfaceState @@ -15737,10 +15786,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject let correlationId = Int64.random(in: 0 ..< Int64.max) let updatedMessage = message - .withUpdatedReplyToMessageId(replyMessageId) + .withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) .withUpdatedCorrelationId(correlationId) var usedCorrelationId = false @@ -15763,7 +15812,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, usedCorrelationId ? correlationId : nil) @@ -15874,12 +15923,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, usedCorrelationId ? correlationId : nil) - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) strongSelf.recorderFeedback?.tap() strongSelf.recorderFeedback = nil @@ -15971,12 +16020,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.collapseInput() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedRecordedMediaPreview(nil).updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedRecordedMediaPreview(nil).updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) } }, nil) - let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] let transformedMessages: [EnqueueMessage] if let silentPosting = silentPosting { @@ -18480,11 +18529,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - if let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId { - if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(before: replyMessageId) { + if let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject { + if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(before: replyMessageSubject.messageId) { strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in var updatedState = state.updatedInterfaceState({ state in - return state.withUpdatedReplyMessageId(message.id) + return state.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( + messageId: message.id, + quote: nil + )) }) if updatedState.inputMode == .none { updatedState = updatedState.updatedInputMode({ _ in .text }) @@ -18500,7 +18552,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let lastMessage = strongSelf.chatDisplayNode.historyNode.latestMessageInCurrentHistoryView() strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in var updatedState = state.updatedInterfaceState({ state in - return state.withUpdatedReplyMessageId(lastMessage?.id) + return state.withUpdatedReplyMessageSubject((lastMessage?.id).flatMap { id in + return ChatInterfaceState.ReplyMessageSubject( + messageId: id, + quote: nil + ) + }) }) if updatedState.inputMode == .none { updatedState = updatedState.updatedInputMode({ _ in .text }) @@ -18526,29 +18583,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - if let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId { + if let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject { let lastMessage = strongSelf.chatDisplayNode.historyNode.latestMessageInCurrentHistoryView() - var updatedReplyMessageId: MessageId? - if replyMessageId == lastMessage?.id { - updatedReplyMessageId = nil - } else if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(after: replyMessageId) { - updatedReplyMessageId = message.id + var updatedReplyMessageSubject: ChatInterfaceState.ReplyMessageSubject? + if replyMessageSubject.messageId == lastMessage?.id { + updatedReplyMessageSubject = nil + } else if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(after: replyMessageSubject.messageId) { + updatedReplyMessageSubject = ChatInterfaceState.ReplyMessageSubject(messageId: message.id, quote: nil) } strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in var updatedState = state.updatedInterfaceState({ state in - return state.withUpdatedReplyMessageId(updatedReplyMessageId) + return state.withUpdatedReplyMessageSubject(updatedReplyMessageSubject) }) if updatedState.inputMode == .none { updatedState = updatedState.updatedInputMode({ _ in .text }) - } else if updatedReplyMessageId == nil { + } else if updatedReplyMessageSubject == nil { updatedState = updatedState.updatedInputMode({ _ in .none }) } return updatedState }) - if let updatedReplyMessageId = updatedReplyMessageId { - strongSelf.navigateToMessage(messageLocation: .id(updatedReplyMessageId, nil), animated: true) + if let updatedReplyMessageSubject { + strongSelf.navigateToMessage(messageLocation: .id(updatedReplyMessageSubject.messageId, nil), animated: true) } } } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 1cf5007752..f2d1330761 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1355,7 +1355,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { accessoryPanelNode.dismiss = { [weak self, weak accessoryPanelNode] in if let strongSelf = self, let accessoryPanelNode = accessoryPanelNode, strongSelf.accessoryPanelNode === accessoryPanelNode { if let _ = accessoryPanelNode as? ReplyAccessoryPanelNode { - strongSelf.requestUpdateChatInterfaceState(.animated(duration: 0.4, curve: .spring), false, { $0.withUpdatedReplyMessageId(nil) }) + strongSelf.requestUpdateChatInterfaceState(.animated(duration: 0.4, curve: .spring), false, { $0.withUpdatedReplyMessageSubject(nil) }) } else if let _ = accessoryPanelNode as? ForwardAccessoryPanelNode { strongSelf.requestUpdateChatInterfaceState(.animated(duration: 0.4, curve: .spring), false, { $0.withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil) }) } else if let _ = accessoryPanelNode as? EditAccessoryPanelNode { @@ -2744,8 +2744,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - func textInputNode() -> EditableTextNode? { - return self.textInputPanelNode?.textInputNode + func textInputView() -> UITextView? { + return self.textInputPanelNode?.textInputNode?.textView } func updateRecordedMediaDeleted(_ isDeleted: Bool) { @@ -3250,7 +3250,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let trimmedInputText = effectiveInputText.string.trimmingCharacters(in: .whitespacesAndNewlines) let peerId = effectivePresentationInterfaceState.chatLocation.peerId if peerId?.namespace != Namespaces.Peer.SecretChat, let interactiveEmojis = self.interactiveEmojis, interactiveEmojis.emojis.contains(trimmedInputText), effectiveInputText.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) == nil { - messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: trimmedInputText)), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: trimmedInputText)), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } else { let inputText = convertMarkdownToAttributes(effectiveInputText) @@ -3283,7 +3283,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { bubbleUpEmojiOrStickersets.removeAll() } - messages.append(.message(text: text.string, attributes: attributes, inlineStickers: inlineStickers, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)) + messages.append(.message(text: text.string, attributes: attributes, inlineStickers: inlineStickers, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)) } } @@ -3339,7 +3339,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.ignoreUpdateHeight = true textInputPanelNode.text = "" - strongSelf.requestUpdateChatInterfaceState(.immediate, true, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeDisableUrlPreview(nil) }) + strongSelf.requestUpdateChatInterfaceState(.immediate, true, { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeDisableUrlPreview(nil) }) strongSelf.ignoreUpdateHeight = false } }, usedCorrelationId) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index b09e9d5e1a..349a6aeb1b 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -67,13 +67,13 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS panelNode.interfaceInteraction = interfaceInteraction return panelNode } - } else if let replyMessageId = chatPresentationInterfaceState.interfaceState.replyMessageId { - if let replyPanelNode = currentPanel as? ReplyAccessoryPanelNode, replyPanelNode.messageId == replyMessageId { + } else if let replyMessageSubject = chatPresentationInterfaceState.interfaceState.replyMessageSubject { + if let replyPanelNode = currentPanel as? ReplyAccessoryPanelNode, replyPanelNode.messageId == replyMessageSubject.messageId && replyPanelNode.quote == replyMessageSubject.quote { replyPanelNode.interfaceInteraction = interfaceInteraction replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return replyPanelNode } else { - let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) + let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) panelNode.interfaceInteraction = interfaceInteraction return panelNode } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 4d1fad96c5..7ba9ca8092 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1106,7 +1106,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuTranslate, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - controllerInteraction.performTextSelectionAction(!isCopyProtected, NSAttributedString(string: messageText), .translate) + controllerInteraction.performTextSelectionAction(message, !isCopyProtected, NSAttributedString(string: messageText), .translate) f(.default) }))) } @@ -1120,7 +1120,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let translation = message.attributes.first(where: { ($0 as? TranslationMessageAttribute)?.toLang == translationState.toLang }) as? TranslationMessageAttribute, !translation.text.isEmpty { text = translation.text } - controllerInteraction.performTextSelectionAction(!isCopyProtected, NSAttributedString(string: text), .speak) + controllerInteraction.performTextSelectionAction(message, !isCopyProtected, NSAttributedString(string: text), .speak) f(.default) }))) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 13d0bec58b..a99524636c 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -182,7 +182,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } } - } else if let isGeneralThreadClosed = chatPresentationInterfaceState.isGeneralThreadClosed, isGeneralThreadClosed && chatPresentationInterfaceState.interfaceState.replyMessageId == nil { + } else if let isGeneralThreadClosed = chatPresentationInterfaceState.isGeneralThreadClosed, isGeneralThreadClosed && chatPresentationInterfaceState.interfaceState.replyMessageSubject == nil { if !canManage { if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { return (currentPanel, nil) @@ -253,7 +253,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if channel.flags.contains(.isForum) { /*if let _ = chatPresentationInterfaceState.threadData { } else { - if chatPresentationInterfaceState.interfaceState.replyMessageId == nil { + if chatPresentationInterfaceState.interfaceState.replyMessageSubject == nil { if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { return (currentPanel, nil) } else { diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 1e95c26958..a5181021e4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1118,7 +1118,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let font = Font.regular(fontSizeForEmojiString(item.message.text)) let textColor = item.presentationData.theme.theme.list.itemPrimaryTextColor - let attributedText = stringWithAppliedEntities(item.message.text, entities: item.message.textEntitiesAttribute?.entities ?? [], baseColor: textColor, linkColor: textColor, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font, message: item.message) + let attributedText = stringWithAppliedEntities(item.message.text, entities: item.message.textEntitiesAttribute?.entities ?? [], baseColor: textColor, linkColor: textColor, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font, message: item.message, adjustQuoteFontSize: true) textLayoutAndApply = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural)) imageSize = CGSize(width: textLayoutAndApply!.0.size.width, height: textLayoutAndApply!.0.size.height) @@ -1215,7 +1215,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var viaBotApply: (TextNodeLayout, () -> TextNode)? var threadInfoApply: (CGSize, (Bool) -> ChatMessageThreadInfoNode)? - var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)? + var replyInfoApply: (CGSize, (CGSize, Bool) -> ChatMessageReplyInfoNode)? var needsReplyBackground = false var replyMarkup: ReplyMarkupMessageAttribute? @@ -1236,6 +1236,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } var replyMessage: Message? + var replyQuote: EngineMessageReplyQuote? var replyStory: StoryId? for attribute in item.message.attributes { if let attribute = attribute as? InlineBotMessageAttribute { @@ -1262,6 +1263,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } else { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } + replyQuote = replyAttribute.quote } else if let attribute = attribute as? ReplyStoryAttribute { replyStory = attribute.storyId } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty { @@ -1296,6 +1298,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { context: item.context, type: .standalone, message: replyMessage, + quote: replyQuote, story: replyStory, parentMessage: item.message, constrainedSize: CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude), @@ -1640,12 +1643,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } if let (replyInfoSize, replyInfoApply) = replyInfoApply { - let replyInfoNode = replyInfoApply(synchronousLoads) + let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: replyInfoSize) + + let replyInfoNode = replyInfoApply(replyInfoFrame.size, synchronousLoads) if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) } - let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: replyInfoSize) replyInfoNode.frame = replyInfoFrame messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: messageInfoSize.height + replyInfoSize.height) diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 08a8738693..e8fb3b2f91 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -483,7 +483,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { string.append(NSAttributedString(string: "\n", font: textFont, textColor: messageTheme.primaryTextColor)) } if let entities = entities { - string.append(stringWithAppliedEntities(text, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil)) + string.append(stringWithAppliedEntities(text, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil, adjustQuoteFontSize: true)) } else { string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: messageTheme.primaryTextColor)) } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 8f014c182c..58a953d368 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -1180,7 +1180,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), threadInfoLayout: (ChatMessageThreadInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageThreadInfoNode), forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, ChatMessageForwardInfoNode.StoryData?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), - replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageReplyInfoNode), + replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (CGSize, Bool) -> ChatMessageReplyInfoNode), actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, WallpaperBackgroundNode?, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)), reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)), mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)), @@ -1520,6 +1520,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var inlineBotNameString: String? var replyMessage: Message? + var replyQuote: EngineMessageReplyQuote? var replyStory: StoryId? var replyMarkup: ReplyMarkupMessageAttribute? var authorNameColor: UIColor? @@ -1536,6 +1537,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } else { replyMessage = firstMessage.associatedMessages[attribute.messageId] } + replyQuote = attribute.quote } else if let attribute = attribute as? ReplyStoryAttribute { replyStory = attribute.storyId } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty && !isPreview { @@ -1963,7 +1965,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var threadInfoSizeApply: (CGSize, (Bool) -> ChatMessageThreadInfoNode?) = (CGSize(), { _ in nil }) var replyInfoOriginY: CGFloat = 0.0 - var replyInfoSizeApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode?) = (CGSize(), { _ in nil }) + var replyInfoSizeApply: (CGSize, (CGSize, Bool) -> ChatMessageReplyInfoNode?) = (CGSize(), { _, _ in nil }) var forwardInfoOriginY: CGFloat = 0.0 var forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode?) = (CGSize(), { _ in nil }) @@ -2142,9 +2144,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if !isInstantVideo, hasReply, (replyMessage != nil || replyStory != nil) { if headerSize.height.isZero { - headerSize.height += 6.0 + headerSize.height += 10.0 } else { - headerSize.height += 2.0 + headerSize.height += 1.0 } let sizeAndApply = replyInfoLayout(ChatMessageReplyInfoNode.Arguments( presentationData: item.presentationData, @@ -2152,18 +2154,19 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode context: item.context, type: .bubble(incoming: incoming), message: replyMessage, + quote: replyQuote, story: replyStory, parentMessage: item.message, - constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude), + constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 6.0, height: CGFloat.greatestFiniteMagnitude), animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer, associatedData: item.associatedData )) - replyInfoSizeApply = (sizeAndApply.0, { synchronousLoads in sizeAndApply.1(synchronousLoads) }) + replyInfoSizeApply = (sizeAndApply.0, { realSize, synchronousLoads in sizeAndApply.1(realSize, synchronousLoads) }) replyInfoOriginY = headerSize.height headerSize.width = max(headerSize.width, replyInfoSizeApply.0.width + bubbleWidthInsets) - headerSize.height += replyInfoSizeApply.0.height + 2.0 + headerSize.height += replyInfoSizeApply.0.height + 7.0 } if !headerSize.height.isZero { @@ -2652,7 +2655,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode threadInfoOriginY: CGFloat, forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode?), forwardInfoOriginY: CGFloat, - replyInfoSizeApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode?), + replyInfoSizeApply: (CGSize, (CGSize, Bool) -> ChatMessageReplyInfoNode?), replyInfoOriginY: CGFloat, removedContentNodeIndices: [Int]?, addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]?, @@ -2959,7 +2962,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - if let replyInfoNode = replyInfoSizeApply.1(synchronousLoads) { + let replyInfoFrame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + replyInfoOriginY), size: CGSize(width: backgroundFrame.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 6.0, height: replyInfoSizeApply.0.height)) + if let replyInfoNode = replyInfoSizeApply.1(replyInfoFrame.size, synchronousLoads) { strongSelf.replyInfoNode = replyInfoNode var animateFrame = true if replyInfoNode.supernode == nil { @@ -2973,7 +2977,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } let previousReplyInfoNodeFrame = replyInfoNode.frame - replyInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + replyInfoOriginY), size: CGSize(width: backgroundFrame.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: replyInfoSizeApply.0.height)) + replyInfoNode.frame = replyInfoFrame if case let .System(duration, _) = animation { if animateFrame { replyInfoNode.layer.animateFrame(from: previousReplyInfoNodeFrame, to: replyInfoNode.frame, duration: duration, timingFunction: timingFunction) diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 5bfad15702..6468def148 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -416,7 +416,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize) var viaBotApply: (TextNodeLayout, () -> TextNode)? - var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)? + var replyInfoApply: (CGSize, (CGSize, Bool) -> ChatMessageReplyInfoNode)? var updatedReplyBackgroundNode: NavigationBackgroundNode? var replyMarkup: ReplyMarkupMessageAttribute? @@ -443,6 +443,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } var replyMessage: Message? + var replyQuote: EngineMessageReplyQuote? var replyStory: StoryId? for attribute in item.message.attributes { if let attribute = attribute as? InlineBotMessageAttribute { @@ -485,6 +486,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } else { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } + replyQuote = replyAttribute.quote } else if let attribute = attribute as? ReplyStoryAttribute { replyStory = attribute.storyId } else if let _ = attribute as? InlineBotMessageAttribute { @@ -500,6 +502,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD context: item.context, type: .standalone, message: replyMessage, + quote: replyQuote, story: replyStory, parentMessage: item.message, constrainedSize: CGSize(width: max(0, availableWidth), height: CGFloat.greatestFiniteMagnitude), @@ -743,12 +746,13 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } if let (replyInfoSize, replyInfoApply) = replyInfoApply { - let replyInfoNode = replyInfoApply(synchronousLoads) + let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: 8.0 + messageInfoSize.height), size: replyInfoSize) + + let replyInfoNode = replyInfoApply(replyInfoFrame.size, synchronousLoads) if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) } - let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: 8.0 + messageInfoSize.height), size: replyInfoSize) replyInfoNode.frame = replyInfoFrame messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: messageInfoSize.height + replyInfoSize.height) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index c2d29f833f..9c86bb318d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -1803,8 +1803,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { guard let strongSelf = self, let item = strongSelf.arguments else { return } - item.controllerInteraction.performTextSelectionAction(true, text, action) + item.controllerInteraction.performTextSelectionAction(item.message, true, text, action) }) + textSelectionNode.enableQuote = item.controllerInteraction.canSetupReply(item.message) == .reply self.textSelectionNode = textSelectionNode self.textClippingNode.addSubnode(textSelectionNode) self.textClippingNode.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index 28bd988437..8669d67c08 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -239,7 +239,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } var viaBotApply: (TextNodeLayout, () -> TextNode)? - var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)? + var replyInfoApply: (CGSize, (CGSize, Bool) -> ChatMessageReplyInfoNode)? var updatedInstantVideoBackgroundImage: UIImage? let instantVideoBackgroundImage: UIImage? @@ -316,6 +316,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if !ignoreHeaders { var replyMessage: Message? + var replyQuote: EngineMessageReplyQuote? var replyStory: StoryId? for attribute in item.message.attributes { @@ -345,6 +346,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } else { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } + replyQuote = replyAttribute.quote } else if let attribute = attribute as? ReplyStoryAttribute { replyStory = attribute.storyId } @@ -359,6 +361,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { context: item.context, type: .standalone, message: replyMessage, + quote: replyQuote, story: replyStory, parentMessage: item.message, constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), @@ -979,12 +982,13 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } if let (replyInfoSize, replyInfoApply) = replyInfoApply { - let replyInfoNode = replyInfoApply(false) + let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (displayVideoFrame.maxX - width + 5.0) : (width - messageInfoSize.width - bubbleEdgeInset - 9.0 + 10.0)), y: 8.0 + messageInfoSize.height), size: replyInfoSize) + + let replyInfoNode = replyInfoApply(replyInfoFrame.size, false) if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode strongSelf.addSubnode(replyInfoNode) } - let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (displayVideoFrame.maxX - width + 5.0) : (width - messageInfoSize.width - bubbleEdgeInset - 9.0 + 10.0)), y: 8.0 + messageInfoSize.height), size: replyInfoSize) animation.animator.updateFrame(layer: replyInfoNode.layer, frame: replyInfoFrame, completion: nil) messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: messageInfoSize.height + replyInfoSize.height) diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index 69d0d55718..e0d67f9a06 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -25,8 +25,8 @@ func chatMessageItemLayoutConstants(_ constants: (ChatMessageItemLayoutConstants let minRadius: CGFloat = 4.0 let maxRadius: CGFloat = 16.0 let radiusTransition = (presentationData.chatBubbleCorners.mainRadius - minRadius) / (maxRadius - minRadius) - let minInset: CGFloat = 9.0 - let maxInset: CGFloat = 12.0 + let minInset: CGFloat = result.text.bubbleInsets.left + let maxInset: CGFloat = 11.0 let textInset: CGFloat = min(maxInset, ceil(maxInset * radiusTransition + minInset * (1.0 - radiusTransition))) result.text.bubbleInsets.left = textInset result.text.bubbleInsets.right = textInset diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift index 3f1699fb42..bbb0ef658c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift @@ -28,6 +28,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { public let context: AccountContext public let type: ChatMessageReplyInfoType public let message: Message? + public let quote: EngineMessageReplyQuote? public let story: StoryId? public let parentMessage: Message public let constrainedSize: CGSize @@ -41,6 +42,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { context: AccountContext, type: ChatMessageReplyInfoType, message: Message?, + quote: EngineMessageReplyQuote?, story: StoryId?, parentMessage: Message, constrainedSize: CGSize, @@ -53,6 +55,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { self.context = context self.type = type self.message = message + self.quote = quote self.story = story self.parentMessage = parentMessage self.constrainedSize = constrainedSize @@ -70,8 +73,8 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } } + private let backgroundView: UIImageView private let contentNode: ASDisplayNode - private let lineNode: ASImageNode private var titleNode: TextNode? private var textNode: TextNodeWithEntities? private var dustNode: InvisibleInkDustNode? @@ -80,24 +83,20 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { private var expiredStoryIconView: UIImageView? override public init() { + self.backgroundView = UIImageView() + self.contentNode = ASDisplayNode() self.contentNode.isUserInteractionEnabled = false self.contentNode.displaysAsynchronously = false self.contentNode.contentMode = .left self.contentNode.contentsScale = UIScreenScale - self.lineNode = ASImageNode() - self.lineNode.displaysAsynchronously = false - self.lineNode.displayWithoutProcessing = true - self.lineNode.isLayerBacked = true - super.init() self.addSubnode(self.contentNode) - self.contentNode.addSubnode(self.lineNode) } - public static func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (Bool) -> ChatMessageReplyInfoNode) { + public static func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (CGSize, Bool) -> ChatMessageReplyInfoNode) { let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode) let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode) let imageNodeLayout = TransformImageNode.asyncLayout(maybeNode?.imageNode) @@ -172,7 +171,6 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { let placeholderColor: UIColor = arguments.parentMessage.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : arguments.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor let titleColor: UIColor - let lineImage: UIImage? let textColor: UIColor let dustColor: UIColor @@ -200,10 +198,20 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } } + let mainColor: UIColor + switch arguments.type { case let .bubble(incoming): titleColor = incoming ? (authorNameColor ?? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor) : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor - lineImage = incoming ? (authorNameColor.flatMap({ PresentationResourcesChat.chatBubbleVerticalLineImage(color: $0) }) ?? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(arguments.presentationData.theme.theme)) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(arguments.presentationData.theme.theme) + if incoming { + if let authorNameColor { + mainColor = authorNameColor + } else { + mainColor = arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor + } + } else { + mainColor = arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor + } if isExpiredStory || isStory { textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor } else if isMedia { @@ -216,8 +224,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) titleColor = serviceColor.primaryText - let graphics = PresentationResourcesChat.additionalGraphics(arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners) - lineImage = graphics.chatServiceVerticalLineImage + mainColor = serviceMessageColorComponents(chatTheme: arguments.presentationData.theme.theme.chat, wallpaper: arguments.presentationData.theme.wallpaper).primaryText textColor = titleColor dustColor = titleColor } @@ -225,8 +232,16 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { let messageText: NSAttributedString if isText, let message = arguments.message { - var text = foldLineBreaks(message.text) - var messageEntities = message.textEntitiesAttribute?.entities ?? [] + var text: String + var messageEntities: [MessageTextEntity] + + if let quote = arguments.quote { + text = quote.text + messageEntities = quote.entities + } else { + text = foldLineBreaks(message.text) + messageEntities = message.textEntitiesAttribute?.entities ?? [] + } if let translateToLanguage = arguments.associatedData.translateToLanguage, !text.isEmpty { for attribute in message.attributes { @@ -310,7 +325,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { imageTextInset += floor(arguments.presentationData.fontSize.baseDisplaySize * 32.0 / 17.0) } - let maximumTextWidth = max(0.0, arguments.constrainedSize.width - imageTextInset) + let maximumTextWidth = max(0.0, arguments.constrainedSize.width - 8.0 - imageTextInset) var contrainedTextSize = CGSize(width: maximumTextWidth, height: arguments.constrainedSize.height) @@ -320,16 +335,28 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { if isExpiredStory || isStory { contrainedTextSize.width -= 26.0 } - let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) + var maxTextNumberOfLines = 1 + var adjustedConstrainedTextSize = contrainedTextSize + var textCutout: TextNodeCutout? + var textCutoutWidth: CGFloat = 0.0 + if arguments.quote != nil { + maxTextNumberOfLines = 5 + if imageTextInset != 0.0 { + adjustedConstrainedTextSize.width += imageTextInset + textCutout = TextNodeCutout(topLeft: CGSize(width: imageTextInset, height: 10.0)) + textCutoutWidth = imageTextInset + 4.0 + } + } + let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: maxTextNumberOfLines, truncationType: .end, constrainedSize: adjustedConstrainedTextSize, alignment: .natural, lineSpacing: 0.05, cutout: textCutout, insets: textInsets)) let imageSide: CGFloat - imageSide = titleLayout.size.height + textLayout.size.height - 12.0 + imageSide = titleLayout.size.height + titleLayout.size.height - 14.0 var applyImage: (() -> TransformImageNode)? if let imageDimensions = imageDimensions { let boundingSize = CGSize(width: imageSide, height: imageSide) leftInset += imageSide + 6.0 - var radius: CGFloat = 6.0 + var radius: CGFloat = 4.0 var imageSize = imageDimensions.aspectFilled(boundingSize) if hasRoundImage { radius = boundingSize.width / 2.0 @@ -374,12 +401,12 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } } - var size = CGSize(width: max(titleLayout.size.width - textInsets.left - textInsets.right, textLayout.size.width - textInsets.left - textInsets.right) + leftInset, height: titleLayout.size.height + textLayout.size.height - 2 * (textInsets.top + textInsets.bottom) + 2 * spacing) + var size = CGSize(width: max(titleLayout.size.width - textInsets.left - textInsets.right, textLayout.size.width - textInsets.left - textInsets.right - textCutoutWidth) + leftInset + 6.0, height: titleLayout.size.height + textLayout.size.height - 2 * (textInsets.top + textInsets.bottom) + 2 * spacing) if isExpiredStory || isStory { size.width += 16.0 } - return (size, { attemptSynchronous in + return (size, { realSize, attemptSynchronous in let node: ChatMessageReplyInfoNode if let maybeNode = maybeNode { node = maybeNode @@ -419,7 +446,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { node.addSubnode(imageNode) node.imageNode = imageNode } - imageNode.frame = CGRect(origin: CGPoint(x: 8.0, y: 3.0), size: CGSize(width: imageSide, height: imageSide)) + imageNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 4.0), size: CGSize(width: imageSide, height: imageSide)) if let updateImageSignal = updateImageSignal { imageNode.setSignal(updateImageSignal) @@ -434,7 +461,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { titleNode.frame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0, y: spacing - textInsets.top + 1.0), size: titleLayout.size) - let textFrame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top - 2.0), size: textLayout.size) + let textFrame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0 - textCutoutWidth, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top - 2.0), size: textLayout.size) textNode.textNode.frame = textFrame.offsetBy(dx: (isExpiredStory || isStory) ? 18.0 : 0.0, dy: 0.0) if isExpiredStory || isStory { @@ -490,9 +517,16 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { dustNode.removeFromSupernode() node.dustNode = nil } - - node.lineNode.image = lineImage - node.lineNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 3.0), size: CGSize(width: 2.0, height: max(0.0, size.height - 4.0))) + + if node.backgroundView.image == nil { + node.backgroundView.image = PresentationResourcesChat.chatReplyBackgroundTemplateImage(arguments.presentationData.theme.theme) + if node.backgroundView.superview == nil { + node.contentNode.view.insertSubview(node.backgroundView, at: 0) + } + } + + node.backgroundView.tintColor = mainColor + node.backgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: realSize.width, height: realSize.height + 2.0)) node.contentNode.frame = CGRect(origin: CGPoint(), size: size) @@ -588,19 +622,19 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } do { - let lineNode = self.lineNode + let backgroundView = self.backgroundView let offset = CGPoint( - x: localRect.minX + sourceReplyPanel.lineNode.frame.minX - lineNode.frame.minX, - y: localRect.minY + sourceReplyPanel.lineNode.frame.minY - lineNode.frame.minY + x: localRect.minX + sourceReplyPanel.lineNode.frame.minX - backgroundView.frame.minX, + y: localRect.minY + sourceReplyPanel.lineNode.frame.minY - backgroundView.frame.minY ) - transition.horizontal.animatePositionAdditive(node: lineNode, offset: CGPoint(x: offset.x, y: 0.0)) - transition.vertical.animatePositionAdditive(node: lineNode, offset: CGPoint(x: 0.0, y: offset.y)) + transition.horizontal.animatePositionAdditive(layer: backgroundView.layer, offset: CGPoint(x: offset.x, y: 0.0)) + transition.vertical.animatePositionAdditive(layer: backgroundView.layer, offset: CGPoint(x: 0.0, y: offset.y)) sourceParentNode.addSubnode(sourceReplyPanel.lineNode) - lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) sourceReplyPanel.lineNode.frame = sourceReplyPanel.lineNode.frame .offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y) diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index e044fbf3f6..50f377f772 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -615,7 +615,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { var viaBotApply: (TextNodeLayout, () -> TextNode)? var threadInfoApply: (CGSize, (Bool) -> ChatMessageThreadInfoNode)? - var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)? + var replyInfoApply: (CGSize, (CGSize, Bool) -> ChatMessageReplyInfoNode)? var replyMarkup: ReplyMarkupMessageAttribute? var availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - max(imageSize.width, 160.0) - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) @@ -638,6 +638,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } var replyMessage: Message? + var replyQuote: EngineMessageReplyQuote? var replyStory: StoryId? for attribute in item.message.attributes { if let attribute = attribute as? InlineBotMessageAttribute { @@ -665,6 +666,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } else { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } + replyQuote = replyAttribute.quote } else if let attribute = attribute as? ReplyStoryAttribute { replyStory = attribute.storyId } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty { @@ -699,6 +701,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { context: item.context, type: .standalone, message: replyMessage, + quote: replyQuote, story: replyStory, parentMessage: item.message, constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), @@ -1051,12 +1054,13 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } if let (replyInfoSize, replyInfoApply) = replyInfoApply { - let replyInfoNode = replyInfoApply(synchronousLoads) + let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: replyInfoSize) + + let replyInfoNode = replyInfoApply(replyInfoFrame.size, synchronousLoads) if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) } - let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: replyInfoSize) replyInfoNode.frame = replyInfoFrame messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: messageInfoSize.height + replyInfoSize.height) diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 1f85e9a22d..3d498b4c6a 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -526,7 +526,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, scheduleCurrentMessage: { }, sendScheduledMessagesNow: { _ in }, editScheduledMessagesTime: { _ in - }, performTextSelectionAction: { _, _, _ in + }, performTextSelectionAction: { _, _, _, _ in }, displayImportedMessageTooltip: { _ in }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 87a1a70247..377a8f710d 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -38,6 +38,7 @@ import SolidRoundedButtonNode import TooltipUI import ChatTextInputMediaRecordingButton import ChatContextQuery +import ChatInputTextNode private let accessoryButtonFont = Font.medium(14.0) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) @@ -468,7 +469,7 @@ final class ChatTextViewForOverlayContent: UIView, ChatInputPanelViewForOverlayC } } -class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { +class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate { let clippingNode: ASDisplayNode var textPlaceholderNode: ImmediateTextNode var textLockIconNode: ASImageNode? @@ -476,7 +477,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode? let textInputContainerBackgroundNode: ASImageNode let textInputContainer: ASDisplayNode - var textInputNode: EditableTextNode? + var textInputNode: ChatInputTextNode? var dustNode: InvisibleInkDustNode? var customEmojiContainerView: CustomEmojiContainerView? @@ -557,7 +558,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var storedInputLanguage: String? var effectiveInputLanguage: String? { if let textInputNode = textInputNode, textInputNode.isFirstResponder() { - return textInputNode.textInputMode.primaryLanguage + return textInputNode.textInputMode?.primaryLanguage } else { return self.storedInputLanguage } @@ -673,7 +674,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) if let presentationInterfaceState = self.presentationInterfaceState { - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) } self.updatingInputState = false @@ -704,12 +705,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(baseFontSize), textColor: textColor) - self.editableTextNodeDidUpdateText(textInputNode) + self.chatInputTextNodeDidUpdateText() } } } - private let textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0) + private let textInputViewInternalInsets: UIEdgeInsets private let accessoryButtonSpacing: CGFloat = 0.0 private let accessoryButtonInset: CGFloat = 2.0 @@ -726,6 +727,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { self.presentationInterfaceState = presentationInterfaceState self.presentationContext = presentationContext + + self.textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0) var hasSpoilers = true if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat { @@ -1024,6 +1027,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.statusDisposable.dispose() self.startingBotDisposable.dispose() self.tooltipController?.dismiss() + self.currentEmojiSuggestion?.disposable.dispose() } func loadTextInputNodeIfNeeded() { @@ -1033,13 +1037,20 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } private func loadTextInputNode() { - let textInputNode = EditableChatTextNode() + let textInputNode = ChatInputTextNode() textInputNode.initialPrimaryLanguage = self.presentationInterfaceState?.interfaceState.inputLanguage var textColor: UIColor = .black var tintColor: UIColor = .blue var baseFontSize: CGFloat = 17.0 var keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default if let presentationInterfaceState = self.presentationInterfaceState { + textInputNode.textView.theme = ChatInputTextView.Theme( + quote: ChatInputTextView.Theme.Quote( + background: presentationInterfaceState.theme.list.itemAccentColor.withMultipliedAlpha(presentationInterfaceState.theme.overallDarkAppearance ? 0.2 : 0.1), + foreground: presentationInterfaceState.theme.list.itemAccentColor + ) + ) + textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor tintColor = presentationInterfaceState.theme.list.itemAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) @@ -1053,7 +1064,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { paragraphStyle.maximumLineHeight = 20.0 paragraphStyle.minimumLineHeight = 20.0 - textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor.rawValue: textColor, NSAttributedString.Key.paragraphStyle.rawValue: paragraphStyle] + textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.paragraphStyle: paragraphStyle] textInputNode.clipsToBounds = false textInputNode.textView.clipsToBounds = false textInputNode.delegate = self @@ -1079,7 +1090,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } if let presentationInterfaceState = self.presentationInterfaceState { - refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth) } @@ -1087,6 +1098,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let textInputFrame = self.textInputContainer.frame textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) + textInputNode.updateLayout(size: textInputNode.bounds.size) textInputNode.view.layoutIfNeeded() self.updateSpoiler() } @@ -1167,8 +1179,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let textFieldHeight: CGFloat if let textInputNode = self.textInputNode { let maxTextWidth = width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - let measuredHeight = textInputNode.measure(CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude)) - let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight.height)) + + //let measuredHeight = textInputNode.measure(CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude)) + let measuredHeight = textInputNode.textHeightForWidth(maxTextWidth) + + let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight)) let maxNumberOfLines = min(12, (Int(fieldMaxHeight - 11.0) - 33) / 22) @@ -1595,7 +1610,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.attributedText = updatedText textInputNode.selectedRange = range } - textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor] + textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor: textColor] self.updateSpoiler() } @@ -1607,6 +1622,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.tintColorDidChange() } + if let textInputNode = self.textInputNode { + textInputNode.textView.theme = ChatInputTextView.Theme( + quote: ChatInputTextView.Theme.Quote( + background: interfaceState.theme.list.itemAccentColor.withMultipliedAlpha(interfaceState.theme.overallDarkAppearance ? 0.2 : 0.1), + foreground: interfaceState.theme.list.itemAccentColor + ) + ) + } + let keyboardAppearance = interfaceState.theme.rootController.keyboardColor.keyboardAppearance if let textInputNode = self.textInputNode, textInputNode.keyboardAppearance != keyboardAppearance { if textInputNode.isFirstResponder() && textInputNode.isCurrentlyEmoji() { @@ -1673,7 +1697,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } let dismissedButtonMessageUpdated = interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != previousState?.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId - let replyMessageUpdated = interfaceState.interfaceState.replyMessageId != previousState?.interfaceState.replyMessageId + let replyMessageUpdated = interfaceState.interfaceState.replyMessageSubject != previousState?.interfaceState.replyMessageSubject if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated || (previousState?.interfaceState.editMessage == nil) != (interfaceState.interfaceState.editMessage == nil) { self.initializedPlaceholder = true @@ -1705,7 +1729,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } if let keyboardButtonsMessage = interfaceState.keyboardButtonsMessage, interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != keyboardButtonsMessage.id { - if keyboardButtonsMessage.requestsSetupReply && keyboardButtonsMessage.id != interfaceState.interfaceState.replyMessageId { + if keyboardButtonsMessage.requestsSetupReply && keyboardButtonsMessage.id != interfaceState.interfaceState.replyMessageSubject?.messageId { } else { if let placeholderValue = interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder, !placeholderValue.isEmpty { placeholder = placeholderValue @@ -2255,6 +2279,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom)) let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size transition.updateFrame(node: textInputNode, frame: textFieldFrame) + textInputNode.updateLayout(size: textFieldFrame.size) self.updateInputField(textInputFrame: textFieldFrame, transition: Transition(transition)) if shouldUpdateLayout { textInputNode.layout() @@ -2513,24 +2538,28 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return prevInputPanelNode is ChatRecordingPreviewInputPanelNode } - @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + func chatInputTextNodeDidUpdateText() { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) - refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) self.updateSpoiler() let inputTextState = self.inputTextState self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) - self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode.primaryLanguage }) + self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode?.primaryLanguage }) self.updateTextNodeText(animated: true) self.updateCounterTextNode(transition: .immediate) } } + @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + self.chatInputTextNodeDidUpdateText() + } + private func updateSpoiler() { guard let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else { return @@ -2678,7 +2707,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.textView.isScrollEnabled = false - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) + refreshChatTextInputAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickers.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?.animatedEmojiStickers.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider) @@ -2783,6 +2812,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } else { beginRequest = true suggestionContext = CurrentEmojiSuggestion(localPosition: trackingPosition, position: emojiSuggestionPosition, disposable: MetaDisposable(), value: nil) + + self.currentEmojiSuggestion?.disposable.dispose() self.currentEmojiSuggestion = suggestionContext } suggestionContext.localPosition = trackingPosition @@ -3442,13 +3473,17 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - @objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { + func chatInputTextNodeShouldReturn() -> Bool { if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendContainerNode.alpha.isZero { self.sendButtonPressed() } return false } + @objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { + return self.chatInputTextNodeShouldReturn() + } + private func applyUpdateSendButtonIcon() { if let interfaceState = self.presentationInterfaceState { let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil @@ -3468,7 +3503,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - @objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { + func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) { if !dueToEditing && !self.updatingInputState { let inputTextState = self.inputTextState self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) @@ -3480,7 +3515,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) self.updateSpoilersRevealed() @@ -3488,7 +3523,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - @objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + @objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { + self.chatInputTextNodeDidChangeSelection(dueToEditing: dueToEditing) + } + + func chatInputTextNodeDidBeginEditing() { guard let interfaceInteraction = self.interfaceInteraction, let presentationInterfaceState = self.presentationInterfaceState else { return } @@ -3509,9 +3548,17 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.inputMenu.activate() } + @objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + self.chatInputTextNodeDidBeginEditing() + } + var skipPresentationInterfaceStateUpdate = false - func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { - self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage + func chatInputTextNodeDidFinishEditing() { + guard let editableTextNode = self.textInputNode else { + return + } + + self.storedInputLanguage = editableTextNode.textInputMode?.primaryLanguage self.inputMenu.deactivate() self.dismissedEmojiSuggestionPosition = nil @@ -3535,6 +3582,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + self.chatInputTextNodeDidFinishEditing() + } + + func chatInputTextNodeBackspaceWhileEmpty() { + } + func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? { if action == makeSelectorFromString("_accessibilitySpeak:") { if case .format = self.inputMenu.state { @@ -3610,8 +3664,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return nil } - @available(iOS 16.0, *) - func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + @available(iOS 13.0, *) + func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + guard let editableTextNode = self.textInputNode else { + return UIMenu(children: []) + } + var actions = suggestedActions if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 { @@ -3678,6 +3736,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return UIMenu(children: actions) } + @available(iOS 16.0, *) + func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { + return chatInputTextNodeMenu(forTextRange: textRange, suggestedActions: suggestedActions) + } + private var currentSpeechHolder: SpeechSynthesizerHolder? @objc func _accessibilitySpeak(_ sender: Any) { var text = "" @@ -3776,7 +3839,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.updateSpoilersRevealed(animated: animated) } - @objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + guard let editableTextNode = self.textInputNode else { + return false + } + self.updateActivity() var cleanText = text let removeSequences: [String] = ["\u{202d}", "\u{202c}"] @@ -3810,7 +3877,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return true } - @objc func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { + @objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + return self.chatInputTextNode(shouldChangeTextIn: range, replacementText: text) + } + + func chatInputTextNodeShouldCopy() -> Bool { self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in storeInputTextInPasteboard(current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count))) return (current, inputMode) @@ -3818,7 +3889,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return false } - @objc func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + @objc func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { + return self.chatInputTextNodeShouldCopy() + } + + func chatInputTextNodeShouldPaste() -> Bool { let pasteboard = UIPasteboard.general var attributedString: NSAttributedString? @@ -3889,6 +3964,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return true } + @objc func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + return self.chatInputTextNodeShouldPaste() + } + @objc func sendButtonPressed() { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength { let textCount = Int32(textInputNode.textView.text.count) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 78cc7bde7b..310fb4da7b 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -135,7 +135,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, scheduleCurrentMessage: { }, sendScheduledMessagesNow: { _ in }, editScheduledMessagesTime: { _ in - }, performTextSelectionAction: { _, _, _ in + }, performTextSelectionAction: { _, _, _, _ in }, displayImportedMessageTooltip: { _ in }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 7df15cad9e..5468b7ee63 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -2871,7 +2871,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, scheduleCurrentMessage: { }, sendScheduledMessagesNow: { _ in }, editScheduledMessagesTime: { _ in - }, performTextSelectionAction: { _, _, _ in + }, performTextSelectionAction: { _, _, _, _ in }, displayImportedMessageTooltip: { _ in }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in diff --git a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift index 725598e990..04581c90a9 100644 --- a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift @@ -21,6 +21,7 @@ import AccessoryPanelNode final class ReplyAccessoryPanelNode: AccessoryPanelNode { private let messageDisposable = MetaDisposable() let messageId: MessageId + let quote: EngineMessageReplyQuote? private var previousMediaReference: AnyMediaReference? @@ -39,8 +40,9 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)? - init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { + init(context: AccountContext, messageId: MessageId, quote: EngineMessageReplyQuote?, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { self.messageId = messageId + self.quote = quote self.context = context self.theme = theme @@ -227,9 +229,15 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { updateImageSignal = .single({ _ in return nil }) } } - - strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string, font: Font.medium(14.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) - strongSelf.textNode.attributedText = messageText + + if let quote = strongSelf.quote { + //TODO:localize + strongSelf.titleNode.attributedText = NSAttributedString(string: "Reply to quote by \(authorName)", font: Font.medium(14.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) + strongSelf.textNode.attributedText = NSAttributedString(string: quote.text, font: textFont, textColor: strongSelf.theme.chat.inputPanel.primaryTextColor) + } else { + strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string, font: Font.medium(14.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) + strongSelf.textNode.attributedText = messageText + } let headerString: String if let message = message, message.flags.contains(.Incoming), let author = message.author { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 8cbcc566ab..5cf2720429 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1508,7 +1508,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, scheduleCurrentMessage: { }, sendScheduledMessagesNow: { _ in }, editScheduledMessagesTime: { _ in - }, performTextSelectionAction: { _, _, _ in + }, performTextSelectionAction: { _, _, _, _ in }, displayImportedMessageTooltip: { _ in }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index e18c83cb7a..87e9402dff 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -118,17 +118,23 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo result.addAttribute(key, value: value, range: range) result.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) } else if key == ChatTextInputAttributes.quote { + fontAttributes.insert(.blockQuote) result.addAttribute(key, value: value, range: range) - result.addAttribute(NSAttributedString.Key.backgroundColor, value: accentTextColor.withAlphaComponent(0.15), range: range) let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.firstLineHeadIndent = 8.0 paragraphStyle.headIndent = 8.0 - paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])] - result.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range) + //paragraphStyle.paragraphSpacing = 8.0 + //paragraphStyle.paragraphSpacingBefore = 8.0 + //result.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range) } } if !fontAttributes.isEmpty { var font: UIFont? + var fontSize = fontSize + if fontAttributes.contains(.blockQuote) { + fontSize = round(fontSize * 0.8235294117647058) + } if fontAttributes == [.bold, .italic, .monospace] { font = Font.semiboldItalicMonospace(fontSize) } else if fontAttributes == [.bold, .monospace] { @@ -143,6 +149,8 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo font = Font.italic(fontSize) } else if fontAttributes == [.monospace] { font = Font.monospace(fontSize) + } else { + font = Font.regular(fontSize) } if let font = font { @@ -660,8 +668,8 @@ private func refreshBlockQuotes(text: NSString, initialAttributedText: NSAttribu } } -public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) { - refreshChatTextInputAttributes(textView: textNode.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)?) { + 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, primaryTextColor: UIColor, accentTextColor: UIColor, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) { @@ -669,6 +677,8 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo return } + textView.textStorage.beginEditing() + var writingDirection: NSWritingDirection? if let style = initialAttributedText.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { writingDirection = style.baseWritingDirection @@ -752,12 +762,130 @@ 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.quote { + fontAttributes.insert(.blockQuote) textView.textStorage.addAttribute(key, value: value, range: range) - textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: accentTextColor.withAlphaComponent(0.15), range: range) let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.firstLineHeadIndent = 8.0 paragraphStyle.headIndent = 8.0 - paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])] - textView.textStorage.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range) + //paragraphStyle.paragraphSpacing = 8.0 + //paragraphStyle.paragraphSpacingBefore = 8.0 + //textView.textStorage.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range) + } + } + + if !fontAttributes.isEmpty { + var font: UIFont? + var baseFontSize = baseFontSize + if fontAttributes.contains(.blockQuote) { + baseFontSize = round(baseFontSize * 0.8235294117647058) + } + if fontAttributes == [.bold, .italic, .monospace] { + font = Font.semiboldItalicMonospace(baseFontSize) + } else if fontAttributes == [.bold, .italic] { + font = Font.semiboldItalic(baseFontSize) + } else if fontAttributes == [.bold, .monospace] { + font = Font.semiboldMonospace(baseFontSize) + } else if fontAttributes == [.italic, .monospace] { + font = Font.italicMonospace(baseFontSize) + } else if fontAttributes == [.bold] { + font = Font.semibold(baseFontSize) + } else if fontAttributes == [.italic] { + font = Font.italic(baseFontSize) + } else if fontAttributes == [.monospace] { + font = Font.monospace(baseFontSize) + } else { + font = Font.regular(baseFontSize) + } + + if let font = font { + textView.textStorage.addAttribute(NSAttributedString.Key.font, value: font, range: range) + } + } + }) + + for (range, attachment) in replaceRanges.sorted(by: { $0.0.location > $1.0.location }) { + textView.textStorage.replaceCharacters(in: range, with: NSAttributedString(attachment: attachment)) + } + } + + textView.textStorage.endEditing() +} + +public func refreshGenericTextInputAttributes(_ textView: UITextView, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, spoilersRevealed: Bool = false) { + guard let initialAttributedText = textView.attributedText, initialAttributedText.length != 0 else { + return + } + + var writingDirection: NSWritingDirection? + if let style = initialAttributedText.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { + writingDirection = style.baseWritingDirection + } + + 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) + + 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) + + if !resultAttributedText.isEqual(to: initialAttributedText) { + textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange) + textView.textStorage.removeAttribute(NSAttributedString.Key.foregroundColor, range: fullRange) + textView.textStorage.removeAttribute(NSAttributedString.Key.backgroundColor, range: fullRange) + textView.textStorage.removeAttribute(NSAttributedString.Key.underlineStyle, range: fullRange) + textView.textStorage.removeAttribute(NSAttributedString.Key.strikethroughStyle, range: fullRange) + textView.textStorage.removeAttribute(ChatTextInputAttributes.textMention, range: fullRange) + textView.textStorage.removeAttribute(ChatTextInputAttributes.textUrl, range: fullRange) + textView.textStorage.removeAttribute(ChatTextInputAttributes.spoiler, range: fullRange) + + textView.textStorage.addAttribute(NSAttributedString.Key.font, value: Font.regular(baseFontSize), range: fullRange) + textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.chat.inputPanel.primaryTextColor, range: fullRange) + + attributedText.enumerateAttributes(in: fullRange, options: [], using: { attributes, range, _ in + var fontAttributes: ChatTextFontAttributes = [] + + for (key, value) in attributes { + if key == ChatTextInputAttributes.textMention || key == ChatTextInputAttributes.textUrl { + textView.textStorage.addAttribute(key, value: value, range: range) + textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.chat.inputPanel.panelControlAccentColor, range: range) + + if theme.chat.inputPanel.panelControlAccentColor.isEqual(theme.chat.inputPanel.primaryTextColor) { + textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) + } + } else if key == ChatTextInputAttributes.bold { + textView.textStorage.addAttribute(key, value: value, range: range) + fontAttributes.insert(.bold) + } else if key == ChatTextInputAttributes.italic { + textView.textStorage.addAttribute(key, value: value, range: range) + fontAttributes.insert(.italic) + } else if key == ChatTextInputAttributes.monospace { + textView.textStorage.addAttribute(key, value: value, range: range) + fontAttributes.insert(.monospace) + } else if key == ChatTextInputAttributes.strikethrough { + textView.textStorage.addAttribute(key, value: value, range: range) + textView.textStorage.addAttribute(NSAttributedString.Key.strikethroughStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) + } else if key == ChatTextInputAttributes.underline { + textView.textStorage.addAttribute(key, value: value, range: range) + textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) + } else if key == ChatTextInputAttributes.spoiler { + textView.textStorage.addAttribute(key, value: value, range: range) + if spoilersRevealed { + textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.primaryTextColor.withAlphaComponent(0.15), range: range) + } else { + textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) + } + } else if key == ChatTextInputAttributes.quote { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.firstLineHeadIndent = 8.0 + paragraphStyle.headIndent = 8.0 + //paragraphStyle.paragraphSpacing = 8.0 + //paragraphStyle.paragraphSpacingBefore = 8.0 + //textView.textStorage.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range) } } @@ -784,113 +912,6 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo } } }) - - for (range, attachment) in replaceRanges.sorted(by: { $0.0.location > $1.0.location }) { - textView.textStorage.replaceCharacters(in: range, with: NSAttributedString(attachment: attachment)) - } - } -} - -public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, spoilersRevealed: Bool = false) { - guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else { - return - } - - var writingDirection: NSWritingDirection? - if let style = initialAttributedText.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { - writingDirection = style.baseWritingDirection - } - - 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) - - 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) - - if !resultAttributedText.isEqual(to: initialAttributedText) { - textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange) - textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.foregroundColor, range: fullRange) - textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.backgroundColor, range: fullRange) - textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.underlineStyle, range: fullRange) - textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.strikethroughStyle, range: fullRange) - textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.textMention, range: fullRange) - textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.textUrl, range: fullRange) - textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.spoiler, range: fullRange) - - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.font, value: Font.regular(baseFontSize), range: fullRange) - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.chat.inputPanel.primaryTextColor, range: fullRange) - - attributedText.enumerateAttributes(in: fullRange, options: [], using: { attributes, range, _ in - var fontAttributes: ChatTextFontAttributes = [] - - for (key, value) in attributes { - if key == ChatTextInputAttributes.textMention || key == ChatTextInputAttributes.textUrl { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.chat.inputPanel.panelControlAccentColor, range: range) - - if theme.chat.inputPanel.panelControlAccentColor.isEqual(theme.chat.inputPanel.primaryTextColor) { - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) - } - } else if key == ChatTextInputAttributes.bold { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) - fontAttributes.insert(.bold) - } else if key == ChatTextInputAttributes.italic { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) - fontAttributes.insert(.italic) - } else if key == ChatTextInputAttributes.monospace { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) - fontAttributes.insert(.monospace) - } else if key == ChatTextInputAttributes.strikethrough { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.strikethroughStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) - } else if key == ChatTextInputAttributes.underline { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) - } else if key == ChatTextInputAttributes.spoiler { - textNode.textView.textStorage.addAttribute(key, value: value, range: range) - if spoilersRevealed { - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.primaryTextColor.withAlphaComponent(0.15), range: range) - } else { - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) - } - } else if key == ChatTextInputAttributes.quote { - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.15), range: range) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.headIndent = 8.0 - paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])] - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range) - } - } - - if !fontAttributes.isEmpty { - var font: UIFont? - if fontAttributes == [.bold, .italic, .monospace] { - font = Font.semiboldItalicMonospace(baseFontSize) - } else if fontAttributes == [.bold, .italic] { - font = Font.semiboldItalic(baseFontSize) - } else if fontAttributes == [.bold, .monospace] { - font = Font.semiboldMonospace(baseFontSize) - } else if fontAttributes == [.italic, .monospace] { - font = Font.italicMonospace(baseFontSize) - } else if fontAttributes == [.bold] { - font = Font.semibold(baseFontSize) - } else if fontAttributes == [.italic] { - font = Font.italic(baseFontSize) - } else if fontAttributes == [.monospace] { - font = Font.monospace(baseFontSize) - } - - if let font = font { - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.font, value: font, range: range) - } - } - }) } } @@ -919,7 +940,7 @@ public func refreshChatTextInputTypingAttributes(_ textView: UITextView, textCol textView.typingAttributes = filteredAttributes } -public func refreshChatTextInputTypingAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat) { +public func refreshChatTextInputTypingAttributes(_ textView: UITextView, theme: PresentationTheme, baseFontSize: CGFloat) { var filteredAttributes: [NSAttributedString.Key: Any] = [ NSAttributedString.Key.font: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor: theme.chat.inputPanel.primaryTextColor @@ -927,8 +948,8 @@ public func refreshChatTextInputTypingAttributes(_ textNode: ASEditableTextNode, let style = NSMutableParagraphStyle() style.baseWritingDirection = .natural filteredAttributes[NSAttributedString.Key.paragraphStyle] = style - if let attributedText = textNode.attributedText, attributedText.length != 0 { - let attributes = attributedText.attributes(at: max(0, min(textNode.selectedRange.location - 1, attributedText.length - 1)), effectiveRange: nil) + if let attributedText = textView.attributedText, attributedText.length != 0 { + let attributes = attributedText.attributes(at: max(0, min(textView.selectedRange.location - 1, attributedText.length - 1)), effectiveRange: nil) for (key, value) in attributes { if key == ChatTextInputAttributes.bold { filteredAttributes[key] = value @@ -941,7 +962,7 @@ public func refreshChatTextInputTypingAttributes(_ textNode: ASEditableTextNode, } } } - textNode.textView.typingAttributes = filteredAttributes + textView.typingAttributes = filteredAttributes } private func trimRangesForChatInputText(_ text: NSAttributedString) -> (Int, Int) { diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index 29c3108be3..aee4ca11d9 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -55,7 +55,7 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M return string } -public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseQuoteTintColor: UIColor? = nil, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:]) -> NSAttributedString { +public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseQuoteTintColor: UIColor? = nil, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:], adjustQuoteFontSize: Bool = false) -> NSAttributedString { let baseQuoteTintColor = baseQuoteTintColor ?? baseColor var nsString: NSString? @@ -289,9 +289,8 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti func addFont(ranges: [NSRange], fontAttributes: ChatTextFontAttributes) { for range in ranges { var font: UIFont? - if fontAttributes.contains(.blockQuote) { - font = baseFont.withSize(round(baseFont.pointSize * 0.8235294117647058)) - } else if fontAttributes == [.bold, .italic] { + + if fontAttributes == [.bold, .italic] { font = boldItalicFont } else if fontAttributes == [.bold] { font = boldFont @@ -299,7 +298,14 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti } else if fontAttributes == [.italic] { font = italicFont addedAttributes.append((range, fontAttributes)) + } else { + font = baseFont } + + if adjustQuoteFontSize, let fontValue = font, fontAttributes.contains(.blockQuote) { + font = fontValue.withSize(round(fontValue.pointSize * 0.8235294117647058)) + } + if let font = font { string.addAttribute(NSAttributedString.Key.font, value: font, range: range) } diff --git a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift index 5e19476099..7306d18fc2 100644 --- a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift +++ b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift @@ -204,12 +204,13 @@ public final class TextSelectionNodeView: UIView { } } -public enum TextSelectionAction { +public enum TextSelectionAction: Equatable { case copy case share case lookup case speak case translate + case quote(range: Range) } public final class TextSelectionNode: ASDisplayNode { @@ -235,6 +236,7 @@ public final class TextSelectionNode: ASDisplayNode { private var displayLinkAnimator: DisplayLinkAnimator? public var enableLookup: Bool = true + public var enableQuote: Bool = false public var didRecognizeTap: Bool { return self.recognizer?.didRecognizeTap ?? false @@ -572,7 +574,12 @@ public final class TextSelectionNode: ASDisplayNode { self?.performAction(string, .copy) self?.cancelSelection() })) - if self.enableLookup { + if self.enableQuote { + actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuQuote, accessibilityLabel: self.strings.Conversation_ContextMenuQuote), action: { [weak self] in + self?.performAction(string, .quote(range: range.lowerBound ..< range.upperBound)) + self?.cancelSelection() + })) + } else if self.enableLookup { actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuLookUp, accessibilityLabel: self.strings.Conversation_ContextMenuLookUp), action: { [weak self] in self?.performAction(string, .lookup) self?.cancelSelection() diff --git a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift index 213927b960..16d4f3e4aa 100644 --- a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift +++ b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift @@ -199,7 +199,7 @@ final class WatchSendMessageHandler: WatchRequestHandler { if args.replyToMid != 0, let peerId = peerId { replyMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.replyToMid) } - messageSignal = .single((.message(text: args.text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId)) + messageSignal = .single((.message(text: args.text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId)) } else if let args = subscription as? TGBridgeSendLocationMessageSubscription, let location = args.location { let peerId = makePeerIdFromBridgeIdentifier(args.peerId) let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: makeVenue(from: location.venue), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) @@ -720,7 +720,7 @@ final class WatchAudioHandler: WatchRequestHandler { replyMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: replyToMid) } - let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.count), attributes: [.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)])), replyToMessageId: replyMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() + let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.count), attributes: [.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)])), replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() } }) } else {