diff --git a/README.md b/README.md index 2728bbd0fd..79f325aa13 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,28 @@ python3 build-system/Make/Make.py \ --configuration=release_arm64 ``` -## Tips +# FAQ + +## Xcode is stuck at "build-request.json not updated yet" + +Occasionally, you might observe the following message in your build log: +``` +"/Users/xxx/Library/Developer/Xcode/DerivedData/Telegram-xxx/Build/Intermediates.noindex/XCBuildData/xxx.xcbuilddata/build-request.json" not updated yet, waiting... +``` + +Should this occur, simply cancel the ongoing build and initiate a new one. + +## Telegram_xcodeproj: no such package + +Following a system restart, the auto-generated Xcode project might encounter a build failure accompanied by this error: +``` +ERROR: Skipping '@rules_xcodeproj_generated//generator/Telegram/Telegram_xcodeproj:Telegram_xcodeproj': no such package '@rules_xcodeproj_generated//generator/Telegram/Telegram_xcodeproj': BUILD file not found in directory 'generator/Telegram/Telegram_xcodeproj' of external repository @rules_xcodeproj_generated. Add a BUILD file to a directory to mark it as a package. +``` + +If you encounter this issue, re-run the project generation steps in the README. + + +# Tips ## Codesigning is not required for simulator-only builds diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index e4f76e918c..71c74a9935 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10096,6 +10096,8 @@ Sorry for the inconvenience."; "PremiumGift.LabelRecipients_1" = "1 recipient"; "PremiumGift.LabelRecipients_any" = "%d recipients"; +"Conversation.ContextMenuQuote" = "Quote"; + "Message.Giveaway" = "Giveaway"; "GiftLink.LinkSharedToChat" = "Gift link forwarded to **%@**"; diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 26832ba114..a002ecd6b6 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -321,6 +321,7 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { case strikethrough case underline case spoiler + case quote public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) @@ -348,6 +349,8 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { self = .underline case 8: self = .spoiler + case 9: + self = .quote default: assertionFailure() self = .bold @@ -379,6 +382,8 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { try container.encode(7 as Int32, forKey: "t") case .spoiler: try container.encode(8 as Int32, forKey: "t") + case .quote: + try container.encode(0 as Int32, forKey: "t") } } } @@ -452,6 +457,9 @@ public struct ChatTextInputStateText: Codable, Equatable { parsedAttributes.append(ChatTextInputStateTextAttribute(type: .underline, range: range.location ..< (range.location + range.length))) } else if key == ChatTextInputAttributes.spoiler { parsedAttributes.append(ChatTextInputStateTextAttribute(type: .spoiler, range: range.location ..< (range.location + range.length))) + } else if key == ChatTextInputAttributes.quote, let value = value as? ChatTextInputTextQuoteAttribute { + let _ = value + parsedAttributes.append(ChatTextInputStateTextAttribute(type: .quote, range: range.location ..< (range.location + range.length))) } } }) @@ -496,6 +504,8 @@ public struct ChatTextInputStateText: Codable, Equatable { result.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) case .spoiler: result.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + case .quote: + result.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) } } return result 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 bba12025a8..1b8754c8ee 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm +++ b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm @@ -16,17 +16,47 @@ @implementation ASCustomTextContainer +- (instancetype)initWithSize:(CGSize)size textStorage:(NSTextStorage *)textStorage { + self = [super initWithSize:size]; + if (self != nil) { + } + 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; } @@ -139,8 +169,7 @@ components.layoutManager = layoutManager; [components.textStorage addLayoutManager:components.layoutManager]; - components.textContainer = [[ASCustomTextContainer alloc] initWithSize:textContainerSize]; - //components.textContainer.exclusionPaths = @[[UIBezierPath bezierPathWithRect:CGRectMake(textContainerSize.width - 60.0, 0.0, 60.0, 40.0)]]; + components.textContainer = [[ASCustomTextContainer alloc] initWithSize:textContainerSize textStorage:textStorage]; components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view. [components.layoutManager addTextContainer:components.textContainer]; diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitContext.h b/submodules/AsyncDisplayKit/Source/ASTextKitContext.h index 5decde42f7..13134b763b 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitContext.h +++ b/submodules/AsyncDisplayKit/Source/ASTextKitContext.h @@ -52,6 +52,8 @@ AS_SUBCLASSING_RESTRICTED @interface ASCustomTextContainer : NSTextContainer +- (instancetype)initWithSize:(CGSize)size textStorage:(NSTextStorage *)textStorage; + @end #endif diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm b/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm index 8e9c276fef..aa9dada4fa 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm +++ b/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm @@ -58,7 +58,7 @@ [_textStorage setAttributedString:attributedString]; } - _textContainer = [[ASCustomTextContainer alloc] initWithSize:constrainedSize]; + _textContainer = [[ASCustomTextContainer alloc] initWithSize:constrainedSize textStorage:nil]; // We want the text laid out up to the very edges of the container. _textContainer.lineFragmentPadding = 0; _textContainer.lineBreakMode = lineBreakMode; 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/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 2720f98e2c..0fe0add1fa 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -13,6 +13,7 @@ import UIKitRuntimeUtils import MediaResources import LegacyMessageInputPanel import LegacyMessageInputPanelInputView +import AttachmentTextInputPanelNode public enum AttachmentButtonType: Equatable { case gallery @@ -185,7 +186,7 @@ public class AttachmentController: ViewController { private let initialButton: AttachmentButtonType private let fromMenu: Bool private let hasTextInput: Bool - private let makeEntityInputView: () -> LegacyMessageInputPanelInputView? + private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? public var animateAppearance: Bool = false public var willDismiss: () -> Void = {} @@ -210,7 +211,7 @@ public class AttachmentController: ViewController { private let dim: ASDisplayNode private let shadowNode: ASImageNode fileprivate let container: AttachmentContainer - private let makeEntityInputView: () -> LegacyMessageInputPanelInputView? + private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? let panel: AttachmentPanel private var currentType: AttachmentButtonType? @@ -280,7 +281,7 @@ public class AttachmentController: ViewController { private let wrapperNode: ASDisplayNode - init(controller: AttachmentController, makeEntityInputView: @escaping () -> LegacyMessageInputPanelInputView?) { + init(controller: AttachmentController, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { self.controller = controller self.makeEntityInputView = makeEntityInputView @@ -911,7 +912,7 @@ public class AttachmentController: ViewController { public var getSourceRect: (() -> CGRect?)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatLocation: ChatLocation?, isScheduledMessages: Bool = false, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, makeEntityInputView: @escaping () -> LegacyMessageInputPanelInputView? = { return nil}) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatLocation: ChatLocation?, isScheduledMessages: Bool = false, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil}) { self.context = context self.updatedPresentationData = updatedPresentationData self.chatLocation = chatLocation diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index a477ad4ec2..5891529a26 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -688,7 +688,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { private var presentationInterfaceState: ChatPresentationInterfaceState private var interfaceInteraction: ChatPanelInterfaceInteraction? - private let makeEntityInputView: () -> LegacyMessageInputPanelInputView? + private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? private let containerNode: ASDisplayNode private let backgroundNode: NavigationBackgroundNode @@ -696,7 +696,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { private let separatorNode: ASDisplayNode private var buttonViews: [Int: ComponentHostView] = [:] - private var textInputPanelNode: LegacyMessageInputPanelNode? + private var textInputPanelNode: AttachmentTextInputPanelNode? private var progressNode: LoadingProgressNode? private var mainButtonNode: MainButtonNode @@ -730,7 +730,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { var mainButtonPressed: () -> Void = { } - init(context: AccountContext, chatLocation: ChatLocation?, isScheduledMessages: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal)?, makeEntityInputView: @escaping () -> LegacyMessageInputPanelInputView?) { + init(context: AccountContext, chatLocation: ChatLocation?, isScheduledMessages: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { self.context = context self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.isScheduledMessages = isScheduledMessages @@ -891,7 +891,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { }) } if let textInputPanelNode = strongSelf.textInputPanelNode { - textInputPanelNode.activateInput() + textInputPanelNode.ensureFocused() } strongSelf.updateChatPresentationInterfaceState(animated: true, { state in return state.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({ @@ -904,53 +904,52 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { } }, reportPeerIrrelevantGeoLocation: { }, displaySlowmodeTooltip: { _, _ in - }, displaySendMessageOptions: { node, gesture in - let _ = node - let _ = gesture -// guard let strongSelf = self, let textInputPanelNode = strongSelf.textInputPanelNode else { -// return -// } -// guard let textInputNode = textInputPanelNode.textInputNode, let peerId = chatLocation?.peerId else { -// return -// } -// -// var hasEntityKeyboard = false -// if case .media = strongSelf.presentationInterfaceState.inputMode { -// hasEntityKeyboard = true -// } -// let _ = (strongSelf.context.account.viewTracker.peerView(peerId) -// |> take(1) -// |> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in -// guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else { -// return -// } -// var sendWhenOnlineAvailable = false -// if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status { -// let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) -// if currentTime > until { -// sendWhenOnlineAvailable = true -// } -// } -// if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { -// 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: { -// }, sendMessage: { [weak textInputPanelNode] mode in -// switch mode { -// case .generic: -// textInputPanelNode?.sendMessage(.generic) -// case .silently: -// textInputPanelNode?.sendMessage(.silent) -// case .whenOnline: -// textInputPanelNode?.sendMessage(.whenOnline) -// } -// }, schedule: { [weak textInputPanelNode] in -// textInputPanelNode?.sendMessage(.schedule) -// }) -// controller.emojiViewProvider = textInputPanelNode.emojiViewProvider -// strongSelf.presentInGlobalOverlay(controller) -// }) + }, displaySendMessageOptions: { [weak self] node, gesture in + guard let strongSelf = self, let textInputPanelNode = strongSelf.textInputPanelNode else { + return + } + textInputPanelNode.loadTextInputNodeIfNeeded() + guard let textInputNode = textInputPanelNode.textInputNode, let peerId = chatLocation?.peerId else { + return + } + + var hasEntityKeyboard = false + if case .media = strongSelf.presentationInterfaceState.inputMode { + hasEntityKeyboard = true + } + let _ = (strongSelf.context.account.viewTracker.peerView(peerId) + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in + guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else { + return + } + var sendWhenOnlineAvailable = false + if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if currentTime > until { + sendWhenOnlineAvailable = true + } + } + if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { + sendWhenOnlineAvailable = false + } + + 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: + textInputPanelNode?.sendMessage(.generic) + case .silently: + textInputPanelNode?.sendMessage(.silent) + case .whenOnline: + textInputPanelNode?.sendMessage(.whenOnline) + } + }, schedule: { [weak textInputPanelNode] in + textInputPanelNode?.sendMessage(.schedule) + }) + controller.emojiViewProvider = textInputPanelNode.emojiViewProvider + strongSelf.presentInGlobalOverlay(controller) + }) }, openScheduledMessages: { }, openPeersNearby: { }, displaySearchResultsTooltip: { _, _ in @@ -1030,7 +1029,10 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { } func updateCaption(_ caption: NSAttributedString) { - self.textInputPanelNode?.setCaption(caption) + if !caption.string.isEmpty { + self.loadTextNodeIfNeeded() + } + self.updateChatPresentationInterfaceState(animated: false, { $0.updatedInterfaceState { $0.withUpdatedComposeInputState(ChatTextInputState(inputText: caption))} }) } private func updateChatPresentationInterfaceState(animated: Bool = true, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { @@ -1038,16 +1040,16 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { } private func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { -// let presentationInterfaceState = f(self.presentationInterfaceState) -// let updateInputTextState = self.presentationInterfaceState.interfaceState.effectiveInputState != presentationInterfaceState.interfaceState.effectiveInputState -// -// self.presentationInterfaceState = presentationInterfaceState -// -// if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { -// textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated) -// -// self.textUpdated(presentationInterfaceState.interfaceState.effectiveInputState.inputText) -// } + let presentationInterfaceState = f(self.presentationInterfaceState) + let updateInputTextState = self.presentationInterfaceState.interfaceState.effectiveInputState != presentationInterfaceState.interfaceState.effectiveInputState + + self.presentationInterfaceState = presentationInterfaceState + + if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { + textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated) + + self.textUpdated(presentationInterfaceState.interfaceState.effectiveInputState.inputText) + } } func updateSelectedIndex(_ index: Int) { @@ -1202,52 +1204,32 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { private func loadTextNodeIfNeeded() { if let _ = self.textInputPanelNode { } else { - - let textInputPanelNode = LegacyMessageInputPanelNode( - context: self.context, - chatLocation: self.presentationInterfaceState.chatLocation, - isScheduledMessages: self.isScheduledMessages, - present: { [weak self] c in - if let strongSelf = self { - strongSelf.present(c) - } - }, - presentInGlobalOverlay: { [weak self] c in - if let strongSelf = self { - strongSelf.present(c) - } - }, - makeEntityInputView: self.makeEntityInputView - ) + let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, isAttachment: true, isScheduledMessages: self.isScheduledMessages, presentController: { [weak self] c in + if let strongSelf = self { + strongSelf.present(c) + } + }, makeEntityInputView: self.makeEntityInputView) + textInputPanelNode.interfaceInteraction = self.interfaceInteraction + textInputPanelNode.sendMessage = { [weak self] mode in + if let strongSelf = self { + strongSelf.sendMessagePressed(mode) + } + } + textInputPanelNode.focusUpdated = { [weak self] focus in + if let strongSelf = self, focus { + strongSelf.beganTextEditing() + } + } + textInputPanelNode.updateHeight = { [weak self] _ in + if let strongSelf = self { + strongSelf.requestLayout() + } + } self.addSubnode(textInputPanelNode) self.textInputPanelNode = textInputPanelNode -// let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, isAttachment: true, isScheduledMessages: self.isScheduledMessages, presentController: { [weak self] c in -// if let strongSelf = self { -// strongSelf.present(c) -// } -// }, makeEntityInputView: self.makeEntityInputView) -// textInputPanelNode.interfaceInteraction = self.interfaceInteraction -// textInputPanelNode.sendMessage = { [weak self] mode in -// if let strongSelf = self { -// strongSelf.sendMessagePressed(mode) -// } -// } -// textInputPanelNode.focusUpdated = { [weak self] focus in -// if let strongSelf = self, focus { -// strongSelf.beganTextEditing() -// } -// } -// textInputPanelNode.updateHeight = { [weak self] _ in -// if let strongSelf = self { -// strongSelf.requestLayout() -// } -// } -// self.addSubnode(textInputPanelNode) -// self.textInputPanelNode = textInputPanelNode -// -// textInputPanelNode.alpha = self.isSelecting ? 1.0 : 0.0 -// textInputPanelNode.isUserInteractionEnabled = self.isSelecting + textInputPanelNode.alpha = self.isSelecting ? 1.0 : 0.0 + textInputPanelNode.isUserInteractionEnabled = self.isSelecting } } @@ -1420,7 +1402,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { if isSelecting { self.loadTextNodeIfNeeded() } else { - let _ = self.textInputPanelNode?.dismissInput() + self.textInputPanelNode?.ensureUnfocused() } var textPanelHeight: CGFloat = 0.0 if let textInputPanelNode = self.textInputPanelNode { @@ -1430,7 +1412,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { if textInputPanelNode.frame.width.isZero { panelTransition = .immediate } - let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, bottomInset: 0.0, keyboardHeight: layout.inputHeight ?? 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, metrics: layout.metrics, isMediaInputExpanded: false) + let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false) let panelFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: panelHeight) if textInputPanelNode.frame.width.isZero { textInputPanelNode.frame = panelFrame 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 b4fd3e9b6d..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 { @@ -106,3 +116,29 @@ public func chatTextInputAddMentionAttribute(_ state: ChatTextInputState, peer: return state } } + +public func chatTextInputAddQuoteAttribute(_ state: ChatTextInputState, selectionRange: Range) -> ChatTextInputState { + if selectionRange.isEmpty { + return state + } + let nsRange = NSRange(location: selectionRange.lowerBound, length: selectionRange.count) + var quoteRange = nsRange + var attributesToRemove: [(NSAttributedString.Key, NSRange)] = [] + state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in + for (key, _) in attributes { + if key == ChatTextInputAttributes.quote { + attributesToRemove.append((key, range)) + quoteRange = quoteRange.union(range) + } else { + attributesToRemove.append((key, nsRange)) + } + } + } + + let result = NSMutableAttributedString(attributedString: state.inputText) + for (attribute, range) in attributesToRemove { + result.removeAttribute(attribute, range: range) + } + result.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: nsRange) + return ChatTextInputState(inputText: result, selectionRange: selectionRange) +} 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/InteractiveTransitionGestureRecognizer.swift b/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift index 56120b515e..f95238e842 100644 --- a/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift +++ b/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift @@ -61,8 +61,11 @@ public enum InteractiveTransitionGestureRecognizerEdgeWidth { } public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { - private let edgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth + private let staticEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth private let allowedDirections: (CGPoint) -> InteractiveTransitionGestureRecognizerDirections + public var dynamicEdgeWidth: ((CGPoint) -> InteractiveTransitionGestureRecognizerEdgeWidth)? + + private var currentEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth private var validatedGesture = false private var firstLocation: CGPoint = CGPoint() @@ -70,7 +73,8 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { public init(target: Any?, action: Selector?, allowedDirections: @escaping (CGPoint) -> InteractiveTransitionGestureRecognizerDirections, edgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth = .constant(16.0)) { self.allowedDirections = allowedDirections - self.edgeWidth = edgeWidth + self.staticEdgeWidth = edgeWidth + self.currentEdgeWidth = edgeWidth super.init(target: target, action: action) @@ -99,6 +103,10 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { return } + if let dynamicEdgeWidth = self.dynamicEdgeWidth { + self.currentEdgeWidth = dynamicEdgeWidth(point) + } + super.touchesBegan(touches, with: event) self.firstLocation = point @@ -150,37 +158,54 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { } } } else { - let edgeWidth: CGFloat - switch self.edgeWidth { + let defaultEdgeWidth: CGFloat + switch self.staticEdgeWidth { case let .constant(value): - edgeWidth = value + defaultEdgeWidth = value case let .widthMultiplier(factor, minValue, maxValue): - edgeWidth = max(minValue, min(size.width * factor, maxValue)) + defaultEdgeWidth = max(minValue, min(size.width * factor, maxValue)) + } + + let extendedEdgeWidth: CGFloat + switch self.currentEdgeWidth { + case let .constant(value): + extendedEdgeWidth = value + case let .widthMultiplier(factor, minValue, maxValue): + extendedEdgeWidth = max(minValue, min(size.width * factor, maxValue)) } if !self.validatedGesture { - if self.firstLocation.x < edgeWidth && !self.currentAllowedDirections.contains(.rightEdge) { + if self.firstLocation.x < extendedEdgeWidth && !self.currentAllowedDirections.contains(.rightEdge) { self.state = .failed return } - if self.firstLocation.x > size.width - edgeWidth && !self.currentAllowedDirections.contains(.leftEdge) { + if self.firstLocation.x > size.width - extendedEdgeWidth && !self.currentAllowedDirections.contains(.leftEdge) { self.state = .failed return } - if self.currentAllowedDirections.contains(.rightEdge) && self.firstLocation.x < edgeWidth { - self.validatedGesture = true - } else if self.currentAllowedDirections.contains(.leftEdge) && self.firstLocation.x > size.width - edgeWidth { - self.validatedGesture = true - } else if !self.currentAllowedDirections.contains(.leftCenter) && translation.x < 0.0 { - self.state = .failed - } else if !self.currentAllowedDirections.contains(.rightCenter) && translation.x > 0.0 { - self.state = .failed - } else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 { - self.state = .failed - } else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX { - self.validatedGesture = true - fireBegan = true + if self.currentAllowedDirections.contains(.rightEdge) && self.firstLocation.x < extendedEdgeWidth { + if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 { + self.state = .failed + } else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX { + self.validatedGesture = true + fireBegan = true + } + } else { + if self.currentAllowedDirections.contains(.rightEdge) && self.firstLocation.x < defaultEdgeWidth { + self.validatedGesture = true + } else if self.currentAllowedDirections.contains(.leftEdge) && self.firstLocation.x > size.width - defaultEdgeWidth { + self.validatedGesture = true + } else if !self.currentAllowedDirections.contains(.leftCenter) && translation.x < 0.0 { + self.state = .failed + } else if !self.currentAllowedDirections.contains(.rightCenter) && translation.x > 0.0 { + self.state = .failed + } else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 { + self.state = .failed + } else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX { + self.validatedGesture = true + fireBegan = true + } } } } diff --git a/submodules/Display/Source/Navigation/NavigationContainer.swift b/submodules/Display/Source/Navigation/NavigationContainer.swift index 683547b170..2a9614ff24 100644 --- a/submodules/Display/Source/Navigation/NavigationContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationContainer.swift @@ -142,6 +142,12 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega } return .right }) + panRecognizer.dynamicEdgeWidth = { [weak self] _ in + guard let self, let controller = self.controllers.last, let value = controller.interactiveNavivationGestureEdgeWidth else { + return .constant(16.0) + } + return value + } if #available(iOS 13.4, *) { panRecognizer.allowedScrollTypesMask = .continuous } diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 32517738df..de32a60afb 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -2,9 +2,14 @@ import Foundation import UIKit import AsyncDisplayKit import CoreText +import AppBundle private let defaultFont = UIFont.systemFont(ofSize: 15.0) +private let quoteIcon: UIImage = { + return UIImage(bundleImageName: "Chat/Message/ReplyQuoteIcon")!.precomposed() +}() + private final class TextNodeStrikethrough { let range: NSRange let frame: CGRect @@ -62,10 +67,48 @@ public struct TextRangeRectEdge: Equatable { } } +public final class TextNodeBlockQuoteData: NSObject { + public let id: Int + public let title: NSAttributedString? + public let color: UIColor + + public init(id: Int, title: NSAttributedString?, color: UIColor) { + self.id = id + self.title = title + self.color = color + + super.init() + } + + override public func isEqual(_ object: Any?) -> Bool { + guard let other = object as? TextNodeBlockQuoteData else { + return false + } + + if self.id != other.id { + return false + } + if let lhsTitle = self.title, let rhsTitle = other.title { + if !lhsTitle.isEqual(to: rhsTitle) { + return false + } + } else if (self.title == nil) != (other.title == nil) { + return false + } + if !self.color.isEqual(other.color) { + return false + } + + return true + } +} + private final class TextNodeLine { let line: CTLine - let frame: CGRect - let range: NSRange + var frame: CGRect + let ascent: CGFloat + let descent: CGFloat + let range: NSRange? let isRTL: Bool let strikethroughs: [TextNodeStrikethrough] let spoilers: [TextNodeSpoiler] @@ -74,9 +117,11 @@ private final class TextNodeLine { let attachments: [TextNodeAttachment] let additionalTrailingLine: (CTLine, Double)? - init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler], spoilerWords: [TextNodeSpoiler], embeddedItems: [TextNodeEmbeddedItem], attachments: [TextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { + init(line: CTLine, frame: CGRect, ascent: CGFloat, descent: CGFloat, range: NSRange?, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler], spoilerWords: [TextNodeSpoiler], embeddedItems: [TextNodeEmbeddedItem], attachments: [TextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { self.line = line self.frame = frame + self.ascent = ascent + self.descent = descent self.range = range self.isRTL = isRTL self.strikethroughs = strikethroughs @@ -90,9 +135,11 @@ private final class TextNodeLine { private final class TextNodeBlockQuote { let frame: CGRect + let tintColor: UIColor - init(frame: CGRect) { + init(frame: CGRect, tintColor: UIColor) { self.frame = frame + self.tintColor = tintColor } } @@ -402,7 +449,14 @@ public final class TextNodeLayout: NSObject { public var trailingLineWidth: CGFloat { if let lastLine = self.lines.last { - return lastLine.frame.maxX + var width = lastLine.frame.maxX + + for blockQuote in self.blockQuotes { + if lastLine.frame.intersects(blockQuote.frame) { + width = max(width, blockQuote.frame.maxX) + } + } + return width } else { return 0.0 } @@ -424,7 +478,7 @@ public final class TextNodeLayout: NSObject { var closestLine: (Int, CGRect, CGFloat)? for line in self.lines { lineIndex += 1 - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) switch self.resolvedAlignment { case .center: lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) @@ -492,7 +546,7 @@ public final class TextNodeLayout: NSObject { var lineIndex = -1 for line in self.lines { lineIndex += 1 - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) switch self.resolvedAlignment { case .center: lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) @@ -559,7 +613,7 @@ public final class TextNodeLayout: NSObject { } } if index >= 0 && index < attributedString.length { - if index < line.range.location + line.range.length { + if let range = line.range, index < range.location + range.length { return (index, attributedString.attributes(at: index, effectiveRange: nil)) } } @@ -569,7 +623,7 @@ public final class TextNodeLayout: NSObject { lineIndex = -1 for line in self.lines { lineIndex += 1 - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) switch self.resolvedAlignment { case .center: lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) @@ -636,7 +690,7 @@ public final class TextNodeLayout: NSObject { } } if index >= 0 && index < attributedString.length { - if index < line.range.location + line.range.length { + if let range = line.range, index < range.location + range.length { return (index, attributedString.attributes(at: index, effectiveRange: nil)) } } @@ -667,14 +721,17 @@ public final class TextNodeLayout: NSObject { var rects: [CGRect] = [] let range = NSRange(stringRange, in: searchText) for line in self.lines { - let lineRange = NSIntersectionRange(range, line.range) + guard let rangeValue = line.range else { + continue + } + let lineRange = NSIntersectionRange(range, rangeValue) if lineRange.length != 0 { var leftOffset: CGFloat = 0.0 - if lineRange.location != line.range.location { + if lineRange.location != rangeValue.location { leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) } var rightOffset: CGFloat = line.frame.width - if lineRange.location + lineRange.length != line.range.length { + if lineRange.location + lineRange.length != rangeValue.length { var secondaryOffset: CGFloat = 0.0 let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset) rightOffset = ceil(rawOffset) @@ -682,7 +739,7 @@ public final class TextNodeLayout: NSObject { rightOffset = ceil(secondaryOffset) } } - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) let width = abs(rightOffset - leftOffset) @@ -716,14 +773,17 @@ public final class TextNodeLayout: NSObject { if let value = value, range.length != 0 { var coveringRect = CGRect() for line in self.lines { - let lineRange = NSIntersectionRange(range, line.range) + guard let rangeValue = line.range else { + continue + } + let lineRange = NSIntersectionRange(range, rangeValue) if lineRange.length != 0 { var leftOffset: CGFloat = 0.0 - if lineRange.location != line.range.location { + if lineRange.location != rangeValue.location { leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) } var rightOffset: CGFloat = line.frame.width - if lineRange.location + lineRange.length != line.range.length { + if lineRange.location + lineRange.length != rangeValue.length { var secondaryOffset: CGFloat = 0.0 let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset) rightOffset = ceil(rawOffset) @@ -732,7 +792,7 @@ public final class TextNodeLayout: NSObject { } } - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) switch self.resolvedAlignment { case .center: lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) @@ -765,14 +825,17 @@ public final class TextNodeLayout: NSObject { if range.length != 0 { var rects: [(CGRect, CGRect)] = [] for line in self.lines { - let lineRange = NSIntersectionRange(range, line.range) + guard let rangeValue = line.range else { + continue + } + let lineRange = NSIntersectionRange(range, rangeValue) if lineRange.length != 0 { var leftOffset: CGFloat = 0.0 - if lineRange.location != line.range.location || line.isRTL { + if lineRange.location != rangeValue.location || line.isRTL { leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) } var rightOffset: CGFloat = line.frame.width - if lineRange.location + lineRange.length != line.range.length || line.isRTL { + if lineRange.location + lineRange.length != rangeValue.length || line.isRTL { var secondaryOffset: CGFloat = 0.0 let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset) rightOffset = ceil(rawOffset) @@ -780,7 +843,7 @@ public final class TextNodeLayout: NSObject { rightOffset = ceil(secondaryOffset) } } - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) @@ -806,14 +869,17 @@ public final class TextNodeLayout: NSObject { var startEdge: TextRangeRectEdge? var endEdge: TextRangeRectEdge? for line in self.lines { - let lineRange = NSIntersectionRange(range, line.range) + guard let rangeValue = line.range else { + continue + } + let lineRange = NSIntersectionRange(range, rangeValue) if lineRange.length != 0 { var leftOffset: CGFloat = 0.0 - if lineRange.location != line.range.location || line.isRTL { + if lineRange.location != rangeValue.location || line.isRTL { leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) } var rightOffset: CGFloat = line.frame.width - if lineRange.location + lineRange.length != line.range.upperBound || line.isRTL { + if lineRange.location + lineRange.length != rangeValue.upperBound || line.isRTL { var secondaryOffset: CGFloat = 0.0 let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset) rightOffset = ceil(rawOffset) @@ -821,19 +887,19 @@ public final class TextNodeLayout: NSObject { rightOffset = ceil(secondaryOffset) } } - var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + line.descent), size: line.frame.size) lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) let width = max(0.0, abs(rightOffset - leftOffset)) - if line.range.contains(range.lowerBound) { + if rangeValue.contains(range.lowerBound) { let offsetX = floor(CTLineGetOffsetForStringIndex(line.line, range.lowerBound, nil)) startEdge = TextRangeRectEdge(x: lineFrame.minX + offsetX, y: lineFrame.minY, height: lineFrame.height) } - if line.range.contains(range.upperBound - 1) { + if rangeValue.contains(range.upperBound - 1) { let offsetX: CGFloat - if line.range.upperBound == range.upperBound { + if rangeValue.upperBound == range.upperBound { offsetX = lineFrame.maxX } else { var secondaryOffset: CGFloat = 0.0 @@ -1022,386 +1088,797 @@ open class TextNode: ASDisplayNode { } } - static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?) -> TextNodeLayout { - if let attributedString = attributedString { - let stringLength = attributedString.length - - let font: CTFont - let resolvedAlignment: NSTextAlignment - - if stringLength != 0 { - if let stringFont = attributedString.attribute(NSAttributedString.Key.font, at: 0, effectiveRange: nil) { - font = stringFont as! CTFont - } else { - font = defaultFont + private static func calculateLayoutV2( + attributedString: NSAttributedString, + minimumNumberOfLines: Int, + maximumNumberOfLines: Int, + truncationType: CTLineTruncationType, + backgroundColor: UIColor?, + constrainedSize: CGSize, + alignment: NSTextAlignment, + verticalAlignment: TextVerticalAlignment, + lineSpacingFactor: CGFloat, + cutout: TextNodeCutout?, + insets: UIEdgeInsets, + lineColor: UIColor?, + textShadowColor: UIColor?, + textShadowBlur: CGFloat?, + textStroke: (UIColor, CGFloat)?, + displaySpoilers: Bool, + displayEmbeddedItemsUnderSpoilers: Bool, + customTruncationToken: NSAttributedString? + ) -> TextNodeLayout { + let blockQuoteLeftInset: CGFloat = 9.0 + let blockQuoteRightInset: CGFloat = 0.0 + let blockQuoteIconInset: CGFloat = 7.0 + + struct StringSegment { + let title: NSAttributedString? + let substring: NSAttributedString + let firstCharacterOffset: Int + let isBlockQuote: Bool + let tintColor: UIColor? + } + var stringSegments: [StringSegment] = [] + + let rawWholeString = attributedString.string as NSString + let wholeStringLength = rawWholeString.length + + var segmentCharacterOffset = 0 + while true { + var found = false + attributedString.enumerateAttribute(NSAttributedString.Key("Attribute__Blockquote"), in: NSRange(location: segmentCharacterOffset, length: wholeStringLength - segmentCharacterOffset), using: { value, effectiveRange, _ in + found = true + if segmentCharacterOffset != effectiveRange.location { + stringSegments.append(StringSegment( + title: nil, + substring: attributedString.attributedSubstring(from: NSRange( + location: segmentCharacterOffset, + length: effectiveRange.location - segmentCharacterOffset + )), + firstCharacterOffset: segmentCharacterOffset, + isBlockQuote: false, + tintColor: nil + )) } - if alignment == .center { - resolvedAlignment = .center - } else { - if let paragraphStyle = attributedString.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { - resolvedAlignment = paragraphStyle.alignment - } else { - resolvedAlignment = alignment + + if let value = value as? TextNodeBlockQuoteData { + if effectiveRange.length != 0 { + stringSegments.append(StringSegment( + title: value.title, + substring: attributedString.attributedSubstring(from: effectiveRange), + firstCharacterOffset: effectiveRange.location, + isBlockQuote: true, + tintColor: value.color + )) } + } else { + stringSegments.append(StringSegment( + title: nil, + substring: attributedString.attributedSubstring(from: effectiveRange), + firstCharacterOffset: effectiveRange.location, + isBlockQuote: false, + tintColor: nil + )) + } + + segmentCharacterOffset = effectiveRange.location + effectiveRange.length + }) + if !found { + if segmentCharacterOffset != wholeStringLength { + stringSegments.append(StringSegment( + title: nil, + substring: attributedString.attributedSubstring(from: NSRange( + location: segmentCharacterOffset, + length: wholeStringLength - segmentCharacterOffset + )), + firstCharacterOffset: segmentCharacterOffset, + isBlockQuote: false, + tintColor: nil + )) + } + + break + } + } + + struct CalculatedSegment { + var titleLine: TextNodeLine? + var lines: [TextNodeLine] = [] + var tintColor: UIColor? + var isBlockQuote: Bool = false + var additionalWidth: CGFloat = 0.0 + } + + var calculatedSegments: [CalculatedSegment] = [] + + for segment in stringSegments { + var calculatedSegment = CalculatedSegment() + calculatedSegment.isBlockQuote = segment.isBlockQuote + calculatedSegment.tintColor = segment.tintColor + + let rawSubstring = segment.substring.string as NSString + let substringLength = rawSubstring.length + let typesetter = CTTypesetterCreateWithAttributedString(segment.substring as CFAttributedString) + + var currentLineStartIndex = 0 + + var constrainedSegmentWidth = constrainedSize.width + var additionalOffsetX: CGFloat = 0.0 + if segment.isBlockQuote { + constrainedSegmentWidth -= blockQuoteLeftInset + blockQuoteRightInset + additionalOffsetX += blockQuoteLeftInset + calculatedSegment.additionalWidth += blockQuoteLeftInset + blockQuoteRightInset + } + + var additionalSegmentRightInset = blockQuoteIconInset + + if let title = segment.title { + let rawTitleLine = CTLineCreateWithAttributedString(title) + if let titleLine = CTLineCreateTruncatedLine(rawTitleLine, constrainedSegmentWidth + additionalSegmentRightInset, .end, nil) { + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + let lineWidth = CTLineGetTypographicBounds(titleLine, &lineAscent, &lineDescent, nil) + calculatedSegment.titleLine = TextNodeLine( + line: titleLine, + frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)), + ascent: lineAscent, + descent: lineDescent, + range: nil, + isRTL: false, + strikethroughs: [], + spoilers: [], + spoilerWords: [], + embeddedItems: [], + attachments: [], + additionalTrailingLine: nil + ) + additionalSegmentRightInset = 0.0 + } + } + + while true { + let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, currentLineStartIndex, constrainedSegmentWidth + additionalSegmentRightInset) + + if lineCharacterCount != 0 { + let line = CTTypesetterCreateLine(typesetter, CFRange(location: currentLineStartIndex, length: lineCharacterCount)) + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + let lineWidth = CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, nil) + calculatedSegment.lines.append(TextNodeLine( + line: line, + frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)), + ascent: lineAscent, + descent: lineDescent, + range: NSRange(location: segment.firstCharacterOffset + currentLineStartIndex, length: lineCharacterCount), + isRTL: false, + strikethroughs: [], + spoilers: [], + spoilerWords: [], + embeddedItems: [], + attachments: [], + additionalTrailingLine: nil + )) + } + + additionalSegmentRightInset = 0.0 + + currentLineStartIndex += lineCharacterCount + + if currentLineStartIndex >= substringLength { + break + } + } + + calculatedSegments.append(calculatedSegment) + } + + var size = CGSize() + let isTruncated = false + + for segment in calculatedSegments { + if let titleLine = segment.titleLine { + size.width = max(size.width, titleLine.frame.origin.x + titleLine.frame.width + segment.additionalWidth) + } + for line in segment.lines { + size.width = max(size.width, line.frame.origin.x + line.frame.width + segment.additionalWidth) + } + } + + var lines: [TextNodeLine] = [] + var blockQuotes: [TextNodeBlockQuote] = [] + for i in 0 ..< calculatedSegments.count { + let segment = calculatedSegments[i] + if i != 0 { + if segment.isBlockQuote { + size.height += 6.0 } + } else { + if segment.isBlockQuote { + size.height += 7.0 + } + } + + let blockMinY = size.height - insets.bottom + var blockWidth: CGFloat = 0.0 + + if let titleLine = segment.titleLine { + titleLine.frame = CGRect(origin: CGPoint(x: titleLine.frame.origin.x, y: -insets.bottom + size.height + titleLine.frame.size.height), size: titleLine.frame.size) + titleLine.frame.size.width += max(0.0, segment.additionalWidth - 2.0) + size.height += titleLine.frame.height + blockWidth = max(blockWidth, titleLine.frame.origin.x + titleLine.frame.width) + + lines.append(titleLine) + } + + for line in segment.lines { + line.frame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: -insets.bottom + size.height + line.frame.size.height), size: line.frame.size) + line.frame.size.width += max(0.0, segment.additionalWidth - 2.0) + //line.frame.size.width = max(blockWidth, line.frame.size.width) + size.height += line.frame.height + blockWidth = max(blockWidth, line.frame.origin.x + line.frame.width) + + lines.append(line) + } + + let blockMaxY = size.height - insets.bottom + + if i != calculatedSegments.count - 1 { + if segment.isBlockQuote { + size.height += 8.0 + } + } else { + if segment.isBlockQuote { + size.height += 6.0 + } + } + + if segment.isBlockQuote, let tintColor = segment.tintColor { + blockQuotes.append(TextNodeBlockQuote(frame: CGRect(origin: CGPoint(x: 0.0, y: blockMinY - 2.0), size: CGSize(width: blockWidth, height: blockMaxY - (blockMinY - 2.0) + 4.0)), tintColor: tintColor)) + } + } + + let rawTextSize = size + size.width += insets.left + insets.right + size.height += insets.top + insets.bottom + + return TextNodeLayout( + attributedString: attributedString, + maximumNumberOfLines: maximumNumberOfLines, + truncationType: truncationType, + constrainedSize: constrainedSize, + explicitAlignment: alignment, + resolvedAlignment: alignment, + verticalAlignment: verticalAlignment, + lineSpacing: lineSpacingFactor, + cutout: cutout, + insets: insets, + size: size, + rawTextSize: rawTextSize, + truncated: isTruncated, + firstLineOffset: lines.first?.descent ?? 0.0, + lines: lines, + blockQuotes: blockQuotes, + backgroundColor: backgroundColor, + lineColor: lineColor, + textShadowColor: textShadowColor, + textShadowBlur: textShadowBlur, + textStroke: textStroke, + displaySpoilers: displaySpoilers + ) + } + + static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?) -> TextNodeLayout { + guard let attributedString else { + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) + } + + if maximumNumberOfLines == 0 { + var found = false + attributedString.enumerateAttribute(NSAttributedString.Key("Attribute__Blockquote"), in: NSRange(location: 0, length: attributedString.length), using: { value, effectiveRange, _ in + if let _ = value as? TextNodeBlockQuoteData { + found = true + } + }) + + if found { + return calculateLayoutV2(attributedString: attributedString, minimumNumberOfLines: minimumNumberOfLines, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, verticalAlignment: verticalAlignment, lineSpacingFactor: lineSpacingFactor, cutout: cutout, insets: insets, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers, displayEmbeddedItemsUnderSpoilers: displayEmbeddedItemsUnderSpoilers, customTruncationToken: customTruncationToken) + } + } + + let stringLength = attributedString.length + + let font: CTFont + let resolvedAlignment: NSTextAlignment + + if stringLength != 0 { + if let stringFont = attributedString.attribute(NSAttributedString.Key.font, at: 0, effectiveRange: nil) { + font = stringFont as! CTFont } else { font = defaultFont - resolvedAlignment = alignment + } + if alignment == .center { + resolvedAlignment = .center + } else { + if let paragraphStyle = attributedString.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { + resolvedAlignment = paragraphStyle.alignment + } else { + resolvedAlignment = alignment + } + } + } else { + font = defaultFont + resolvedAlignment = alignment + } + + let fontAscent = CTFontGetAscent(font) + let fontDescent = CTFontGetDescent(font) + let fontLineHeight = floor(fontAscent + fontDescent) + let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor) + + var lines: [TextNodeLine] = [] + let blockQuotes: [TextNodeBlockQuote] = [] + + var maybeTypesetter: CTTypesetter? + maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) + if maybeTypesetter == nil { + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) + } + + let typesetter = maybeTypesetter! + + var lastLineCharacterIndex: CFIndex = 0 + var layoutSize = CGSize() + + var cutoutEnabled = false + var cutoutMinY: CGFloat = 0.0 + var cutoutMaxY: CGFloat = 0.0 + var cutoutWidth: CGFloat = 0.0 + var cutoutOffset: CGFloat = 0.0 + + var bottomCutoutEnabled = false + var bottomCutoutSize = CGSize() + + if let topLeft = cutout?.topLeft { + cutoutMinY = -fontLineSpacing + cutoutMaxY = topLeft.height + fontLineSpacing + cutoutWidth = topLeft.width + cutoutOffset = cutoutWidth + cutoutEnabled = true + } else if let topRight = cutout?.topRight { + cutoutMinY = -fontLineSpacing + cutoutMaxY = topRight.height + fontLineSpacing + cutoutWidth = topRight.width + cutoutEnabled = true + } + + if let bottomRight = cutout?.bottomRight { + bottomCutoutSize = bottomRight + bottomCutoutEnabled = true + } + + let firstLineOffset = floorToScreenPixels(fontDescent) + + var truncated = false + var first = true + while true { + var strikethroughs: [TextNodeStrikethrough] = [] + var spoilers: [TextNodeSpoiler] = [] + var spoilerWords: [TextNodeSpoiler] = [] + var embeddedItems: [TextNodeEmbeddedItem] = [] + var attachments: [TextNodeAttachment] = [] + + var lineConstrainedWidth = constrainedSize.width + var lineConstrainedWidthDelta: CGFloat = 0.0 + var lineOriginY = floorToScreenPixels(layoutSize.height + fontAscent) + if !first { + lineOriginY += fontLineSpacing + } + var lineCutoutOffset: CGFloat = 0.0 + var lineAdditionalWidth: CGFloat = 0.0 + + if cutoutEnabled { + if lineOriginY - fontLineHeight < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY { + lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth) + lineConstrainedWidthDelta = -cutoutWidth + lineCutoutOffset = cutoutOffset + lineAdditionalWidth = cutoutWidth + } } - let fontAscent = CTFontGetAscent(font) - let fontDescent = CTFontGetDescent(font) - let fontLineHeight = floor(fontAscent + fontDescent) - let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor) + let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth)) - var lines: [TextNodeLine] = [] - var blockQuotes: [TextNodeBlockQuote] = [] - - var maybeTypesetter: CTTypesetter? - maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) - if maybeTypesetter == nil { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) + func addSpoiler(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + spoilers.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset), height: ascent + descent))) } - let typesetter = maybeTypesetter! + func addSpoilerWord(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + spoilerWords.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent))) + } - var lastLineCharacterIndex: CFIndex = 0 - var layoutSize = CGSize() + func addEmbeddedItem(item: AnyHashable, line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + embeddedItems.append(TextNodeEmbeddedItem(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), item: item)) + } - var cutoutEnabled = false - var cutoutMinY: CGFloat = 0.0 - var cutoutMaxY: CGFloat = 0.0 - var cutoutWidth: CGFloat = 0.0 - var cutoutOffset: CGFloat = 0.0 + func addAttachment(attachment: UIImage, line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + attachments.append(TextNodeAttachment(range: NSMakeRange(startIndex, endIndex - startIndex), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), attachment: attachment)) + } - var bottomCutoutEnabled = false - var bottomCutoutSize = CGSize() + var isLastLine = false + if maximumNumberOfLines != 0 && lines.count == maximumNumberOfLines - 1 && lineCharacterCount > 0 { + isLastLine = true + } else if layoutSize.height + (fontLineSpacing + fontLineHeight) * 2.0 > constrainedSize.height { + isLastLine = true + } + if isLastLine { + if first { + first = false + } else { + layoutSize.height += fontLineSpacing + } + + var didClipLinebreak = false + var lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) + let nsString = (attributedString.string as NSString) + for i in lineRange.location ..< (lineRange.location + lineRange.length) { + if nsString.character(at: i) == 0x0a { + lineRange.length = max(0, i - lineRange.location) + didClipLinebreak = true + break + } + } + + var brokenLineRange = CFRange(location: lastLineCharacterIndex, length: lineCharacterCount) + if brokenLineRange.location + brokenLineRange.length > attributedString.length { + brokenLineRange.length = attributedString.length - brokenLineRange.location + } + if lineRange.length == 0 && !didClipLinebreak { + break + } + + let coreTextLine: CTLine + let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0) + + var lineConstrainedSize = constrainedSize + lineConstrainedSize.width += lineConstrainedWidthDelta + if bottomCutoutEnabled { + lineConstrainedSize.width -= bottomCutoutSize.width + } + + let truncatedTokenString: NSAttributedString + if let customTruncationToken { + if lineRange.length == 0 && customTruncationToken.string.hasPrefix("\u{2026} ") { + truncatedTokenString = customTruncationToken.attributedSubstring(from: NSRange(location: 2, length: customTruncationToken.length - 2)) + } else { + truncatedTokenString = customTruncationToken + } + } else { + var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:] + truncationTokenAttributes[NSAttributedString.Key.font] = font + truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber + let tokenString = "\u{2026}" + + truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes) + } + let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) + let truncationTokenWidth = CTLineGetTypographicBounds(truncationToken, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(truncationToken) + + var effectiveLineRange = brokenLineRange + var additionalTrailingLine: (CTLine, Double)? + + var measureFitWidth = CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) + if customTruncationToken != nil && lineRange.location + lineRange.length < attributedString.length { + measureFitWidth += truncationTokenWidth + } + + if lineRange.length == 0 || measureFitWidth < Double(lineConstrainedSize.width) { + if didClipLinebreak { + if lineRange.length == 0 { + coreTextLine = CTLineCreateWithAttributedString(NSAttributedString()) + } else { + coreTextLine = originalLine + } + additionalTrailingLine = (truncationToken, truncationTokenWidth) - if let topLeft = cutout?.topLeft { - cutoutMinY = -fontLineSpacing - cutoutMaxY = topLeft.height + fontLineSpacing - cutoutWidth = topLeft.width - cutoutOffset = cutoutWidth - cutoutEnabled = true - } else if let topRight = cutout?.topRight { - cutoutMinY = -fontLineSpacing - cutoutMaxY = topRight.height + fontLineSpacing - cutoutWidth = topRight.width - cutoutEnabled = true - } - - if let bottomRight = cutout?.bottomRight { - bottomCutoutSize = bottomRight - bottomCutoutEnabled = true - } - - let firstLineOffset = floorToScreenPixels(fontDescent) - - var truncated = false - var first = true - while true { - var strikethroughs: [TextNodeStrikethrough] = [] - var spoilers: [TextNodeSpoiler] = [] - var spoilerWords: [TextNodeSpoiler] = [] - var embeddedItems: [TextNodeEmbeddedItem] = [] - var attachments: [TextNodeAttachment] = [] - - var lineConstrainedWidth = constrainedSize.width - var lineConstrainedWidthDelta: CGFloat = 0.0 - var lineOriginY = floorToScreenPixels(layoutSize.height + fontAscent) - if !first { - lineOriginY += fontLineSpacing + truncated = true + } else { + coreTextLine = originalLine + } + } else { + if customTruncationToken != nil { + let coreTextLine1 = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width)), truncationType, truncationToken) ?? truncationToken + let runs = (CTLineGetGlyphRuns(coreTextLine1) as [AnyObject]) as! [CTRun] + var hasTruncationToken = false + for run in runs { + let runRange = CTRunGetStringRange(run) + if runRange.location + runRange.length >= nsString.length { + hasTruncationToken = true + break + } + } + + if hasTruncationToken { + coreTextLine = coreTextLine1 + } else { + let coreTextLine2 = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width) - truncationTokenWidth), truncationType, truncationToken) ?? truncationToken + coreTextLine = coreTextLine2 + } + } else { + coreTextLine = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width)), truncationType, truncationToken) ?? truncationToken + } + let runs = (CTLineGetGlyphRuns(coreTextLine) as [AnyObject]) as! [CTRun] + for run in runs { + let runAttributes: NSDictionary = CTRunGetAttributes(run) + if let _ = runAttributes["CTForegroundColorFromContext"] { + brokenLineRange.length = CTRunGetStringRange(run).location - brokenLineRange.location + break + } + } + if customTruncationToken != nil { + assert(true) + } + effectiveLineRange = CFRange(location: effectiveLineRange.location, length: 0) + for run in runs { + let runRange = CTRunGetStringRange(run) + if runRange.location + runRange.length > brokenLineRange.location + brokenLineRange.length { + continue + } + effectiveLineRange.length = max(effectiveLineRange.length, (runRange.location + runRange.length) - effectiveLineRange.location) + } + + if brokenLineRange.location + brokenLineRange.length > attributedString.length { + brokenLineRange.length = attributedString.length - brokenLineRange.location + } + if effectiveLineRange.location + effectiveLineRange.length > attributedString.length { + effectiveLineRange.length = attributedString.length - effectiveLineRange.location + } + truncated = true } - var lineCutoutOffset: CGFloat = 0.0 - var lineAdditionalWidth: CGFloat = 0.0 - if cutoutEnabled { - if lineOriginY - fontLineHeight < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY { - lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth) - lineConstrainedWidthDelta = -cutoutWidth - lineCutoutOffset = cutoutOffset - lineAdditionalWidth = cutoutWidth + var headIndent: CGFloat = 0.0 + if brokenLineRange.location >= 0 && brokenLineRange.length > 0 && brokenLineRange.location + brokenLineRange.length <= attributedString.length { + attributedString.enumerateAttributes(in: NSMakeRange(brokenLineRange.location, brokenLineRange.length), options: []) { attributes, range, _ in + if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + var startIndex: Int? + var currentIndex: Int? + + let nsString = (attributedString.string as NSString) + nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in + if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { + if let currentStartIndex = startIndex { + startIndex = nil + let endIndex = range.location + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + } + } else if startIndex == nil { + startIndex = range.location + } + currentIndex = range.location + range.length + } + + if let currentStartIndex = startIndex, let currentIndex = currentIndex { + startIndex = nil + let endIndex = currentIndex + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0) + } + + addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { + let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) + let x = lowerX < upperX ? lowerX : upperX + strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) + } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { + headIndent = paragraphStyle.headIndent + } + + if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { + if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } + } + + if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } } } - let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth)) + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) + let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) + layoutSize.height += fontLineHeight + fontLineSpacing - func addSpoiler(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int) { - var secondaryLeftOffset: CGFloat = 0.0 - let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) - var leftOffset = floor(rawLeftOffset) - if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { - leftOffset = floor(secondaryLeftOffset) - } - - var secondaryRightOffset: CGFloat = 0.0 - let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) - var rightOffset = ceil(rawRightOffset) - if !rawRightOffset.isEqual(to: secondaryRightOffset) { - rightOffset = ceil(secondaryRightOffset) - } - - spoilers.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset), height: ascent + descent))) + if let (_, additionalTrailingLineWidth) = additionalTrailingLine { + lineAdditionalWidth += additionalTrailingLineWidth } - func addSpoilerWord(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { - var secondaryLeftOffset: CGFloat = 0.0 - let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) - var leftOffset = floor(rawLeftOffset) - if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { - leftOffset = floor(secondaryLeftOffset) - } - - var secondaryRightOffset: CGFloat = 0.0 - let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) - var rightOffset = ceil(rawRightOffset) - if !rawRightOffset.isEqual(to: secondaryRightOffset) { - rightOffset = ceil(secondaryRightOffset) - } - - spoilerWords.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent))) + layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) + + if headIndent > 0.0 { + //blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) } - func addEmbeddedItem(item: AnyHashable, line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { - var secondaryLeftOffset: CGFloat = 0.0 - let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) - var leftOffset = floor(rawLeftOffset) - if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { - leftOffset = floor(secondaryLeftOffset) + var isRTL = false + let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray + if glyphRuns.count != 0 { + let run = glyphRuns[0] as! CTRun + if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { + isRTL = true } - - var secondaryRightOffset: CGFloat = 0.0 - let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) - var rightOffset = ceil(rawRightOffset) - if !rawRightOffset.isEqual(to: secondaryRightOffset) { - rightOffset = ceil(secondaryRightOffset) - } - - embeddedItems.append(TextNodeEmbeddedItem(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), item: item)) } - func addAttachment(attachment: UIImage, line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { - var secondaryLeftOffset: CGFloat = 0.0 - let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) - var leftOffset = floor(rawLeftOffset) - if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { - leftOffset = floor(secondaryLeftOffset) - } - - var secondaryRightOffset: CGFloat = 0.0 - let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) - var rightOffset = ceil(rawRightOffset) - if !rawRightOffset.isEqual(to: secondaryRightOffset) { - rightOffset = ceil(secondaryRightOffset) - } - - attachments.append(TextNodeAttachment(range: NSMakeRange(startIndex, endIndex - startIndex), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), attachment: attachment)) - } - - var isLastLine = false - if maximumNumberOfLines != 0 && lines.count == maximumNumberOfLines - 1 && lineCharacterCount > 0 { - isLastLine = true - } else if layoutSize.height + (fontLineSpacing + fontLineHeight) * 2.0 > constrainedSize.height { - isLastLine = true - } - if isLastLine { + lines.append(TextNodeLine( + line: coreTextLine, + frame: lineFrame, + ascent: lineAscent, + descent: lineDescent, + range: NSMakeRange(effectiveLineRange.location, effectiveLineRange.length), + isRTL: isRTL, + strikethroughs: strikethroughs, + spoilers: spoilers, + spoilerWords: spoilerWords, + embeddedItems: embeddedItems, + attachments: attachments, + additionalTrailingLine: additionalTrailingLine + )) + break + } else { + if lineCharacterCount > 0 { if first { first = false } else { layoutSize.height += fontLineSpacing } - var didClipLinebreak = false - var lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) - let nsString = (attributedString.string as NSString) - for i in lineRange.location ..< (lineRange.location + lineRange.length) { - if nsString.character(at: i) == 0x0a { - lineRange.length = max(0, i - lineRange.location) - didClipLinebreak = true - break - } + var lineRange = CFRangeMake(lastLineCharacterIndex, lineCharacterCount) + if lineRange.location + lineRange.length > attributedString.length { + lineRange.length = attributedString.length - lineRange.location } - - var brokenLineRange = CFRange(location: lastLineCharacterIndex, length: lineCharacterCount) - if brokenLineRange.location + brokenLineRange.length > attributedString.length { - brokenLineRange.length = attributedString.length - brokenLineRange.location - } - if lineRange.length == 0 && !didClipLinebreak { + if lineRange.length < 0 { break } - - let coreTextLine: CTLine - let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0) - - var lineConstrainedSize = constrainedSize - lineConstrainedSize.width += lineConstrainedWidthDelta - if bottomCutoutEnabled { - lineConstrainedSize.width -= bottomCutoutSize.width - } - - let truncatedTokenString: NSAttributedString - if let customTruncationToken { - if lineRange.length == 0 && customTruncationToken.string.hasPrefix("\u{2026} ") { - truncatedTokenString = customTruncationToken.attributedSubstring(from: NSRange(location: 2, length: customTruncationToken.length - 2)) - } else { - truncatedTokenString = customTruncationToken - } - } else { - var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:] - truncationTokenAttributes[NSAttributedString.Key.font] = font - truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber - let tokenString = "\u{2026}" - - truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes) - } - let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) - let truncationTokenWidth = CTLineGetTypographicBounds(truncationToken, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(truncationToken) - - var effectiveLineRange = brokenLineRange - var additionalTrailingLine: (CTLine, Double)? - - var measureFitWidth = CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) - if customTruncationToken != nil && lineRange.location + lineRange.length < attributedString.length { - measureFitWidth += truncationTokenWidth - } - - if lineRange.length == 0 || measureFitWidth < Double(lineConstrainedSize.width) { - if didClipLinebreak { - if lineRange.length == 0 { - coreTextLine = CTLineCreateWithAttributedString(NSAttributedString()) - } else { - coreTextLine = originalLine - } - additionalTrailingLine = (truncationToken, truncationTokenWidth) - - truncated = true - } else { - coreTextLine = originalLine - } - } else { - if customTruncationToken != nil { - let coreTextLine1 = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width)), truncationType, truncationToken) ?? truncationToken - let runs = (CTLineGetGlyphRuns(coreTextLine1) as [AnyObject]) as! [CTRun] - var hasTruncationToken = false - for run in runs { - let runRange = CTRunGetStringRange(run) - if runRange.location + runRange.length >= nsString.length { - hasTruncationToken = true - break - } - } - - if hasTruncationToken { - coreTextLine = coreTextLine1 - } else { - let coreTextLine2 = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width) - truncationTokenWidth), truncationType, truncationToken) ?? truncationToken - coreTextLine = coreTextLine2 - } - } else { - coreTextLine = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width)), truncationType, truncationToken) ?? truncationToken - } - let runs = (CTLineGetGlyphRuns(coreTextLine) as [AnyObject]) as! [CTRun] - for run in runs { - let runAttributes: NSDictionary = CTRunGetAttributes(run) - if let _ = runAttributes["CTForegroundColorFromContext"] { - brokenLineRange.length = CTRunGetStringRange(run).location - brokenLineRange.location - break - } - } - if customTruncationToken != nil { - assert(true) - } - effectiveLineRange = CFRange(location: effectiveLineRange.location, length: 0) - for run in runs { - let runRange = CTRunGetStringRange(run) - if runRange.location + runRange.length > brokenLineRange.location + brokenLineRange.length { - continue - } - effectiveLineRange.length = max(effectiveLineRange.length, (runRange.location + runRange.length) - effectiveLineRange.location) - } - - if brokenLineRange.location + brokenLineRange.length > attributedString.length { - brokenLineRange.length = attributedString.length - brokenLineRange.location - } - if effectiveLineRange.location + effectiveLineRange.length > attributedString.length { - effectiveLineRange.length = attributedString.length - effectiveLineRange.location - } - truncated = true - } + + let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 100.0) + lastLineCharacterIndex += lineCharacterCount var headIndent: CGFloat = 0.0 - if brokenLineRange.location >= 0 && brokenLineRange.length > 0 && brokenLineRange.location + brokenLineRange.length <= attributedString.length { - attributedString.enumerateAttributes(in: NSMakeRange(brokenLineRange.location, brokenLineRange.length), options: []) { attributes, range, _ in - if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - var startIndex: Int? - var currentIndex: Int? - - let nsString = (attributedString.string as NSString) - nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in - if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { - if let currentStartIndex = startIndex { - startIndex = nil - let endIndex = range.location - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) - } - } else if startIndex == nil { - startIndex = range.location + attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + var startIndex: Int? + var currentIndex: Int? + + let nsString = (attributedString.string as NSString) + nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in + if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { + if let currentStartIndex = startIndex { + startIndex = nil + let endIndex = range.location + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) } - currentIndex = range.location + range.length + } else if startIndex == nil { + startIndex = range.location } - - if let currentStartIndex = startIndex, let currentIndex = currentIndex { - startIndex = nil - let endIndex = currentIndex - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0) - } - - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { - let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) - let x = lowerX < upperX ? lowerX : upperX - strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) - } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { - headIndent = paragraphStyle.headIndent + currentIndex = range.location + range.length } - if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { - if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } + if let currentStartIndex = startIndex, let currentIndex = currentIndex { + startIndex = nil + let endIndex = currentIndex + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) } - if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage { + addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { + let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) + let x = lowerX < upperX ? lowerX : upperX + strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) + } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { + headIndent = paragraphStyle.headIndent + } + + if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { + if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) { var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) } } + + if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } } - let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) - layoutSize.height += fontLineHeight + fontLineSpacing - - if let (_, additionalTrailingLineWidth) = additionalTrailingLine { - lineAdditionalWidth += additionalTrailingLineWidth - } - - layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) + layoutSize.height += fontLineHeight + layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth + headIndent) if headIndent > 0.0 { - blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) + //blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) } var isRTL = false @@ -1413,141 +1890,53 @@ open class TextNode: ASDisplayNode { } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(effectiveLineRange.location, effectiveLineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: embeddedItems, attachments: attachments, additionalTrailingLine: additionalTrailingLine)) - break + lines.append(TextNodeLine( + line: coreTextLine, + frame: lineFrame, + ascent: lineAscent, + descent: lineDescent, + range: NSMakeRange(lineRange.location, lineRange.length), + isRTL: isRTL, + strikethroughs: strikethroughs, + spoilers: spoilers, + spoilerWords: spoilerWords, + embeddedItems: embeddedItems, + attachments: attachments, + additionalTrailingLine: nil + )) } else { - if lineCharacterCount > 0 { - if first { - first = false - } else { - layoutSize.height += fontLineSpacing - } - - var lineRange = CFRangeMake(lastLineCharacterIndex, lineCharacterCount) - if lineRange.location + lineRange.length > attributedString.length { - lineRange.length = attributedString.length - lineRange.location - } - if lineRange.length < 0 { - break - } - - let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 100.0) - lastLineCharacterIndex += lineCharacterCount - - var headIndent: CGFloat = 0.0 - attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in - if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - var startIndex: Int? - var currentIndex: Int? - - let nsString = (attributedString.string as NSString) - nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in - if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { - if let currentStartIndex = startIndex { - startIndex = nil - let endIndex = range.location - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) - } - } else if startIndex == nil { - startIndex = range.location - } - currentIndex = range.location + range.length - } - - if let currentStartIndex = startIndex, let currentIndex = currentIndex { - startIndex = nil - let endIndex = currentIndex - addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) - } - - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { - let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) - let x = lowerX < upperX ? lowerX : upperX - strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) - } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { - headIndent = paragraphStyle.headIndent - } - - if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { - if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } - } - - if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage { - var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - - addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) - } - } - - let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) - let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) - layoutSize.height += fontLineHeight - layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) - - if headIndent > 0.0 { - blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) - } - - var isRTL = false - let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray - if glyphRuns.count != 0 { - let run = glyphRuns[0] as! CTRun - if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { - isRTL = true - } - } - - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: embeddedItems, attachments: attachments, additionalTrailingLine: nil)) - } else { - if !lines.isEmpty { - layoutSize.height += fontLineSpacing - } - break - } - } - } - - let rawLayoutSize = layoutSize - if !lines.isEmpty && bottomCutoutEnabled { - let proposedWidth = lines[lines.count - 1].frame.width + bottomCutoutSize.width - if proposedWidth > layoutSize.width { - if proposedWidth <= constrainedSize.width + .ulpOfOne { - layoutSize.width = proposedWidth - } else { - layoutSize.height += bottomCutoutSize.height - } - } - } - - if lines.count < minimumNumberOfLines { - var lineCount = lines.count - while lineCount < minimumNumberOfLines { - if lineCount != 0 { + if !lines.isEmpty { layoutSize.height += fontLineSpacing } - layoutSize.height += fontLineHeight - lineCount += 1 + break } } - - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), rawTextSize: CGSize(width: ceil(rawLayoutSize.width) + insets.left + insets.right, height: ceil(rawLayoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, blockQuotes: blockQuotes, backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) - } else { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) } + + let rawLayoutSize = layoutSize + if !lines.isEmpty && bottomCutoutEnabled { + let proposedWidth = lines[lines.count - 1].frame.width + bottomCutoutSize.width + if proposedWidth > layoutSize.width { + if proposedWidth <= constrainedSize.width + .ulpOfOne { + layoutSize.width = proposedWidth + } else { + layoutSize.height += bottomCutoutSize.height + } + } + } + + if lines.count < minimumNumberOfLines { + var lineCount = lines.count + while lineCount < minimumNumberOfLines { + if lineCount != 0 { + layoutSize.height += fontLineSpacing + } + layoutSize.height += fontLineHeight + lineCount += 1 + } + } + + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), rawTextSize: CGSize(width: ceil(rawLayoutSize.width) + insets.left + insets.right, height: ceil(rawLayoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, blockQuotes: blockQuotes, backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) } override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { @@ -1584,6 +1973,53 @@ open class TextNode: ASDisplayNode { context.fill(bounds) } + let alignment = layout.resolvedAlignment + var offset = CGPoint(x: layout.insets.left, y: layout.insets.top) + switch layout.verticalAlignment { + case .top: + break + case .middle: + offset.y = floor((bounds.height - layout.size.height) / 2.0) + layout.insets.top + case .bottom: + offset.y = floor(bounds.height - layout.size.height) + layout.insets.top + } + + if !layout.lines.isEmpty { + offset.y += layout.lines[0].descent + } + + for blockQuote in layout.blockQuotes { + let radius: CGFloat = 3.0 + + 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) + context.fillPath() + + 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)) + context.move(to: CGPoint(x: lineFrame.minX, y: lineFrame.minY + radius)) + context.addArc(tangent1End: CGPoint(x: lineFrame.minX, y: lineFrame.minY), tangent2End: CGPoint(x: lineFrame.minX + radius, y: lineFrame.minY), radius: radius) + context.addLine(to: CGPoint(x: lineFrame.minX + radius, y: lineFrame.maxY)) + context.addArc(tangent1End: CGPoint(x: lineFrame.minX, y: lineFrame.maxY), tangent2End: CGPoint(x: lineFrame.minX, y: lineFrame.maxY - radius), radius: radius) + context.closePath() + context.fillPath() + } + if let textShadowColor = layout.textShadowColor { context.setTextDrawingMode(.fill) context.setShadow(offset: layout.textShadowBlur != nil ? .zero : CGSize(width: 0.0, height: 1.0), blur: layout.textShadowBlur ?? 0.0, color: textShadowColor.cgColor) @@ -1605,17 +2041,6 @@ open class TextNode: ASDisplayNode { let textPosition = context.textPosition context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0) - let alignment = layout.resolvedAlignment - var offset = CGPoint(x: layout.insets.left, y: layout.insets.top) - switch layout.verticalAlignment { - case .top: - break - case .middle: - offset.y = floor((bounds.height - layout.size.height) / 2.0) + layout.insets.top - case .bottom: - offset.y = floor(bounds.height - layout.size.height) + layout.insets.top - } - for i in 0 ..< layout.lines.count { let line = layout.lines[i] @@ -1632,6 +2057,12 @@ open class TextNode: ASDisplayNode { lineFrame.origin.x += offset.x } } + + //context.setStrokeColor(UIColor.red.cgColor) + //context.stroke(lineFrame.offsetBy(dx: 0.0, dy: -lineFrame.height)) + + lineFrame.origin.y += -line.descent + context.textPosition = CGPoint(x: lineFrame.minX, y: lineFrame.minY) if layout.displaySpoilers && !line.spoilers.isEmpty { @@ -1688,8 +2119,11 @@ open class TextNode: ASDisplayNode { if !line.strikethroughs.isEmpty { for strikethrough in line.strikethroughs { + guard let lineRange = line.range else { + continue + } var textColor: UIColor? - layout.attributedString?.enumerateAttributes(in: NSMakeRange(line.range.location, line.range.length), options: []) { attributes, range, _ in + layout.attributedString?.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { textColor = color } @@ -1760,34 +2194,6 @@ open class TextNode: ASDisplayNode { } } - var blockQuoteFrames: [CGRect] = [] - var currentBlockQuoteFrame: CGRect? - for blockQuote in layout.blockQuotes { - if let frame = currentBlockQuoteFrame { - if blockQuote.frame.minY - frame.maxY < 20.0 { - currentBlockQuoteFrame = frame.union(blockQuote.frame) - } else { - blockQuoteFrames.append(frame) - currentBlockQuoteFrame = frame - } - } else { - currentBlockQuoteFrame = blockQuote.frame - } - } - - if let frame = currentBlockQuoteFrame { - blockQuoteFrames.append(frame) - } - - for frame in blockQuoteFrames { - if let lineColor = layout.lineColor { - context.setFillColor(lineColor.cgColor) - } - let rect = UIBezierPath(roundedRect: CGRect(x: frame.minX - 9.0, y: frame.minY - 14.0, width: 2.0, height: frame.height), cornerRadius: 1.0) - context.addPath(rect.cgPath) - context.fillPath() - } - context.textMatrix = textMatrix context.textPosition = CGPoint(x: textPosition.x, y: textPosition.y) } @@ -1944,7 +2350,7 @@ open class TextView: UIView { let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor) var lines: [TextNodeLine] = [] - var blockQuotes: [TextNodeBlockQuote] = [] + let blockQuotes: [TextNodeBlockQuote] = [] var maybeTypesetter: CTTypesetter? maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) @@ -2175,13 +2581,15 @@ open class TextView: UIView { } } - let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) layoutSize.height += fontLineHeight + fontLineSpacing layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) if headIndent > 0.0 { - blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) + //blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) } var isRTL = false @@ -2193,7 +2601,20 @@ open class TextView: UIView { } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: [], attachments: attachments, additionalTrailingLine: nil)) + lines.append(TextNodeLine( + line: coreTextLine, + frame: lineFrame, + ascent: lineAscent, + descent: lineDescent, + range: NSMakeRange(lineRange.location, lineRange.length), + isRTL: isRTL, + strikethroughs: strikethroughs, + spoilers: spoilers, + spoilerWords: spoilerWords, + embeddedItems: [], + attachments: attachments, + additionalTrailingLine: nil + )) break } else { if lineCharacterCount > 0 { @@ -2263,13 +2684,15 @@ open class TextView: UIView { } } - let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) + var lineAscent: CGFloat = 0.0 + var lineDescent: CGFloat = 0.0 + let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) layoutSize.height += fontLineHeight layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) if headIndent > 0.0 { - blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) + //blockQuotes.append(TextNodeBlockQuote(frame: lineFrame)) } var isRTL = false @@ -2281,7 +2704,20 @@ open class TextView: UIView { } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: [], attachments: attachments, additionalTrailingLine: nil)) + lines.append(TextNodeLine( + line: coreTextLine, + frame: lineFrame, + ascent: lineAscent, + descent: lineDescent, + range: NSMakeRange(lineRange.location, lineRange.length), + isRTL: isRTL, + strikethroughs: strikethroughs, + spoilers: spoilers, + spoilerWords: spoilerWords, + embeddedItems: [], + attachments: attachments, + additionalTrailingLine: nil + )) } else { if !lines.isEmpty { layoutSize.height += fontLineSpacing diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 5eaecf3ab4..7a7fecd24f 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -227,6 +227,10 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { } private var navigationBarOrigin: CGFloat = 0.0 + + open var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? { + return nil + } open func navigationLayout(layout: ContainerViewLayout) -> NavigationLayout { let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 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/MtProtoKit/Sources/MTProto.m b/submodules/MtProtoKit/Sources/MTProto.m index 91a79f97c2..965ee029d0 100644 --- a/submodules/MtProtoKit/Sources/MTProto.m +++ b/submodules/MtProtoKit/Sources/MTProto.m @@ -838,6 +838,9 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; __weak MTProto *weakSelf = self; MTSignal *checkSignal = [[MTConnectionProbing probeProxyWithContext:_context datacenterId:_datacenterId settings:transport.proxySettings] delay:5.0 onQueue:[MTQueue concurrentDefaultQueue]]; checkSignal = [[checkSignal then:[[MTSignal complete] delay:20.0 onQueue:[MTQueue concurrentDefaultQueue]]] restart]; + if (_probingDisposable == nil) { + _probingDisposable = [[MTMetaDisposable alloc] init]; + } [_probingDisposable setDisposable:[checkSignal startWithNextStrict:^(NSNumber *next) { [[MTProto managerQueue] dispatchOnQueue:^{ __strong MTProto *strongSelf = weakSelf; diff --git a/submodules/Pasteboard/Sources/Pasteboard.swift b/submodules/Pasteboard/Sources/Pasteboard.swift index b4343090bf..fb639dc29a 100644 --- a/submodules/Pasteboard/Sources/Pasteboard.swift +++ b/submodules/Pasteboard/Sources/Pasteboard.swift @@ -64,6 +64,9 @@ private func chatInputStateString(attributedString: NSAttributedString) -> NSAtt if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { string.addAttribute(ChatTextInputAttributes.customEmoji, value: value, range: range) } + if let value = attributes[ChatTextInputAttributes.quote] as? ChatTextInputTextQuoteAttribute { + string.addAttribute(ChatTextInputAttributes.quote, value: value, range: range) + } }) return 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 38aff70d05..4b6b33ff2e 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -261,11 +261,18 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere let peerId: PeerId = chatPeerId.peerId switch replyTo { - case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, _): - let targetId = MessageId(peerId: replyToPeerId?.peerId ?? peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) - var replyIds = ReferencedReplyMessageIds() - replyIds.add(sourceId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id), targetId: targetId) - return (replyIds, []) + case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyToTopId, quoteText, quoteEntities): + let _ = replyHeader + let _ = replyToTopId + let _ = quoteText + let _ = quoteEntities + + if let replyToMsgId = replyToMsgId { + let targetId = MessageId(peerId: replyToPeerId?.peerId ?? peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) + var replyIds = ReferencedReplyMessageIds() + replyIds.add(sourceId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id), targetId: targetId) + return (replyIds, []) + } case .messageReplyStoryHeader: break } @@ -275,11 +282,18 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere case let .messageService(_, id, _, chatPeerId, replyHeader, _, _, _): if let replyHeader = replyHeader { switch replyHeader { - case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, _): - let targetId = MessageId(peerId: replyToPeerId?.peerId ?? chatPeerId.peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) - var replyIds = ReferencedReplyMessageIds() - replyIds.add(sourceId: MessageId(peerId: chatPeerId.peerId, namespace: Namespaces.Message.Cloud, id: id), targetId: targetId) - return (replyIds, []) + case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyToTopId, quoteText, quoteEntities): + let _ = replyHeader + let _ = replyToTopId + let _ = quoteText + let _ = quoteEntities + + if let replyToMsgId = replyToMsgId { + let targetId = MessageId(peerId: replyToPeerId?.peerId ?? chatPeerId.peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) + var replyIds = ReferencedReplyMessageIds() + replyIds.add(sourceId: MessageId(peerId: chatPeerId.peerId, namespace: Namespaces.Message.Cloud, id: id), targetId: targetId) + return (replyIds, []) + } case .messageReplyStoryHeader: break } @@ -553,40 +567,48 @@ extension StoreMessage { if let replyTo = replyTo { var threadMessageId: MessageId? switch replyTo { - case let .messageReplyHeader(flags, replyToMsgId, replyToPeerId, replyToTopId): + case let .messageReplyHeader(flags, replyToMsgId, replyToPeerId, replyHeader, replyToTopId, quoteText, quoteEntities): + let _ = replyHeader let isForumTopic = (flags & (1 << 3)) != 0 - let replyPeerId = replyToPeerId?.peerId ?? peerId - if let replyToTopId = replyToTopId { - if peerIsForum { - if isForumTopic { + 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 { + if peerIsForum { + if isForumTopic { + let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) + threadMessageId = threadIdValue + if replyPeerId == peerId { + threadId = makeMessageThreadId(threadIdValue) + } + } + } else { let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) threadMessageId = threadIdValue if replyPeerId == peerId { threadId = makeMessageThreadId(threadIdValue) } } - } else { - let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) - threadMessageId = threadIdValue - if replyPeerId == peerId { - threadId = makeMessageThreadId(threadIdValue) - } - } - } else if peerId.namespace == Namespaces.Peer.CloudChannel { - let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) - - if peerIsForum { - if isForumTopic { + } else if peerId.namespace == Namespaces.Peer.CloudChannel { + let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) + + if peerIsForum { + if isForumTopic { + threadMessageId = threadIdValue + threadId = makeMessageThreadId(threadIdValue) + } + } else { threadMessageId = threadIdValue threadId = makeMessageThreadId(threadIdValue) } - } else { - threadMessageId = threadIdValue - threadId = makeMessageThreadId(threadIdValue) } + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote)) } - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: 0), threadMessageId: threadMessageId)) case let .messageReplyStoryHeader(userId, storyId): attributes.append(ReplyStoryAttribute(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: storyId))) } @@ -818,26 +840,35 @@ extension StoreMessage { if let replyTo = replyTo { var threadMessageId: MessageId? switch replyTo { - case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyToTopId): - let replyPeerId = replyToPeerId?.peerId ?? peerId - if let replyToTopId = replyToTopId { - let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) - threadMessageId = threadIdValue - if replyPeerId == peerId { + case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyToTopId, quoteText, quoteEntities): + let _ = replyHeader + + 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 { + let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToTopId) + threadMessageId = threadIdValue + if replyPeerId == peerId { + threadId = makeMessageThreadId(threadIdValue) + } + } else if peerId.namespace == Namespaces.Peer.CloudChannel { + let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) + threadMessageId = threadIdValue threadId = makeMessageThreadId(threadIdValue) } - } else if peerId.namespace == Namespaces.Peer.CloudChannel { - let threadIdValue = MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) - threadMessageId = threadIdValue - threadId = makeMessageThreadId(threadIdValue) + switch action { + case .messageActionTopicEdit: + threadId = Int64(replyToMsgId) + default: + break + } + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote)) } - switch action { - case .messageActionTopicEdit: - threadId = Int64(replyToMsgId) - default: - break - } - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId)) 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 fb92377aca..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)) @@ -334,7 +334,7 @@ private func sendUploadedMessageContent(postbox: Postbox, network: Network, stat if threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: threadId.flatMap(Int32.init(clamping:))) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -356,7 +356,7 @@ private func sendUploadedMessageContent(postbox: Postbox, network: Network, stat if threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: threadId.flatMap(Int32.init(clamping:))) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -392,7 +392,7 @@ private func sendUploadedMessageContent(postbox: Postbox, network: Network, stat if threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: threadId.flatMap(Int32.init(clamping:))) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -407,18 +407,18 @@ private func sendUploadedMessageContent(postbox: Postbox, network: Network, stat if let replyMessageId = replyMessageId { let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 replyTo = .inputReplyToStory(userId: inputUser, storyId: replyToStoryId.id) } else { let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } } else { let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } sendMessageRequest = network.request(Api.functions.messages.sendScreenshotNotification(peer: inputPeer, replyTo: replyTo, randomId: uniqueId)) @@ -557,7 +557,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M flags |= 1 << 0 let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 @@ -575,7 +575,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M flags |= 1 << 0 let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 2db702b7cb..e7b968033a 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1523,12 +1523,33 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: switch draft { case .draftMessageEmpty: inputState = nil - case let .draftMessage(_, replyToMsgId, message, entities, date): - var replyToMessageId: MessageId? - if let replyToMsgId = replyToMsgId { - replyToMessageId = MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId) + case let .draftMessage(_, replyToMsgHeader, message, entities, date): + var replySubject: EngineMessageReplySubject? + if let replyToMsgHeader = replyToMsgHeader { + switch replyToMsgHeader { + case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyToTopId, quoteText, quoteEntities): + let _ = replyHeader + let _ = replyToTopId + + var quote: EngineMessageReplyQuote? + if let quoteText = quoteText { + quote = EngineMessageReplyQuote( + text: quoteText, + entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []) + ) + } + + if let replyToMsgId = 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 47dfeebad7..103078dda9 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift @@ -141,19 +141,72 @@ private func synchronizeChatInputState(transaction: Transaction, postbox: Postbo if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { var flags: Int32 = 0 if let inputState = inputState { - if inputState.replyToMessageId != nil { - flags |= (1 << 0) - } if !inputState.entities.isEmpty { flags |= (1 << 3) } } var topMsgId: Int32? if let threadId = threadId { - flags |= (1 << 2) topMsgId = Int32(clamping: threadId) } - return network.request(Api.functions.messages.saveDraft(flags: flags, replyToMsgId: inputState?.replyToMessageId?.id, topMsgId: topMsgId, peer: inputPeer, message: inputState?.text ?? "", entities: apiEntitiesFromMessageTextEntities(inputState?.entities ?? [], associatedPeers: SimpleDictionary()))) + + var replyTo: Api.InputReplyTo? + if let replySubject = inputState?.replySubject { + flags |= 1 << 0 + + var innerFlags: Int32 = 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; + 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()))) |> delay(2.0, queue: Queue.concurrentDefaultQueue()) |> `catch` { _ -> Signal in return .single(.boolFalse) diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index bc3ec25459..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) + + 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:))) + + 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:))) + + 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:))) + + 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 @@ -1251,18 +1347,18 @@ public final class PendingMessageManager { if let replyMessageId = replyMessageId { let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } else if let replyToStoryId = replyToStoryId { if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) { flags |= 1 << 0 replyTo = .inputReplyToStory(userId: inputUser, storyId: replyToStoryId.id) } else { let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } } else { let replyFlags: Int32 = 0 - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: 0, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } sendMessageRequest = network.request(Api.functions.messages.sendScreenshotNotification(peer: inputPeer, replyTo: replyTo, randomId: uniqueId)) 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/Data/TelegramEngineData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift index 53924d3223..87d5c3390f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift @@ -155,6 +155,9 @@ public extension TelegramEngine { return results } } + + /*public func subscribe(_ ts: repeat each T) -> Signal { + }*/ public func subscribe(_ t0: T0) -> Signal { return self._subscribe(items: [t0 as! AnyPostboxViewDataItem]) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 9683f3d5a6..ba6a04cbfe 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -83,7 +83,7 @@ private func keepWebViewSignal(network: Network, stateManager: AccountStateManag if threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyToMessageId.id, topMsgId: threadId.flatMap(Int32.init(clamping:))) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyToMessageId.id, topMsgId: threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } let signal: Signal = network.request(Api.functions.messages.prolongWebView(flags: flags, peer: peer, bot: bot, queryId: queryId, replyTo: replyTo, sendAs: sendAs)) |> mapError { _ -> KeepWebViewError in @@ -157,7 +157,7 @@ func _internal_requestWebView(postbox: Postbox, network: Network, stateManager: if threadId != nil { replyFlags |= 1 << 0 } - replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyToMessageId.id, topMsgId: threadId.flatMap(Int32.init(clamping:))) + replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyToMessageId.id, topMsgId: threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil) } return network.request(Api.functions.messages.requestWebView(flags: flags, peer: inputPeer, bot: inputBot, url: url, startParam: payload, themeParams: serializedThemeParams, platform: botWebViewPlatform, replyTo: replyTo, sendAs: nil)) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift index 6ac6b5101d..26b109bc37 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift @@ -37,13 +37,21 @@ func _internal_clearCloudDraftsInteractively(postbox: Postbox, network: Network, _internal_updateChatInputState(transaction: transaction, peerId: key.peerId, threadId: key.threadId, inputState: nil) if let peer = transaction.getPeer(key.peerId), let inputPeer = apiInputPeer(peer) { - var flags: Int32 = 0 var topMsgId: Int32? if let threadId = key.threadId { - flags |= (1 << 2) topMsgId = Int32(clamping: threadId) } - signals.append(network.request(Api.functions.messages.saveDraft(flags: flags, replyToMsgId: nil, topMsgId: topMsgId, peer: inputPeer, message: "", entities: nil)) + var flags: Int32 = 0 + var replyTo: Api.InputReplyTo? + if let topMsgId = topMsgId { + flags |= (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; + var innerFlags: Int32 = 0 + innerFlags |= 1 << 0 + replyTo = .inputReplyToMessage(flags: innerFlags, replyToMsgId: 0, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil) + } + signals.append(network.request(Api.functions.messages.saveDraft(flags: flags, replyTo: replyTo, peer: inputPeer, message: "", entities: nil)) |> `catch` { _ -> Signal in return .single(.boolFalse) } 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 ebfc636b05..5f67d72241 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -297,6 +297,9 @@ public enum PresentationResourceKey: Int32 { case storyViewListLikeIcon case navigationPostStoryIcon + + case chatReplyBackgroundTemplateImage + case chatReplyServiceBackgroundTemplateImage } public enum ChatExpiredStoryIndicatorType: Hashable { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 9e38d8bdd6..bceb49c555 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1284,4 +1284,36 @@ 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) + }) + } + + public static func chatReplyServiceBackgroundTemplateImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatReplyServiceBackgroundTemplateImage.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: CGSize(width: radius, height: size.height)), cornerRadius: radius).cgPath) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius)).withRenderingMode(.alwaysTemplate) + }) + } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 877ba76ba6..d3ea8e12a5 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -342,6 +342,14 @@ swift_library( "//submodules/TelegramUI/Components/LegacyMessageInputPanel", "//submodules/StatisticsUI", "//submodules/TelegramUI/Components/PremiumGiftAttachmentScreen", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/EditableTokenListNode", + "//submodules/TelegramUI/Components/Chat/ChatInputTextNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/BUILD b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/BUILD new file mode 100644 index 0000000000..881c4fbbf0 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatHistoryEntry", + module_name = "ChatHistoryEntry", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/MergeLists", + "//submodules/TemporaryCachedPeerDataManager", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntry.swift b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift similarity index 93% rename from submodules/TelegramUI/Sources/ChatHistoryEntry.swift rename to submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift index 3c0a1f1cb1..d402913f9e 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntry.swift +++ b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift @@ -12,15 +12,15 @@ public enum ChatMessageEntryContentType { } public struct ChatMessageEntryAttributes: Equatable { - var rank: CachedChannelAdminRank? - var isContact: Bool - var contentTypeHint: ChatMessageEntryContentType - var updatingMedia: ChatUpdatingMessageMedia? - var isPlaying: Bool - var isCentered: Bool - var authorStoryStats: PeerStoryStats? + public var rank: CachedChannelAdminRank? + public var isContact: Bool + public var contentTypeHint: ChatMessageEntryContentType + public var updatingMedia: ChatUpdatingMessageMedia? + public var isPlaying: Bool + public var isCentered: Bool + public var authorStoryStats: PeerStoryStats? - init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?, isPlaying: Bool, isCentered: Bool, authorStoryStats: PeerStoryStats?) { + public init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?, isPlaying: Bool, isCentered: Bool, authorStoryStats: PeerStoryStats?) { self.rank = rank self.isContact = isContact self.contentTypeHint = contentTypeHint @@ -41,7 +41,7 @@ public struct ChatMessageEntryAttributes: Equatable { } } -enum ChatHistoryEntry: Identifiable, Comparable { +public enum ChatHistoryEntry: Identifiable, Comparable { case MessageEntry(Message, ChatPresentationData, Bool, MessageHistoryEntryLocation?, ChatHistoryMessageSelection, ChatMessageEntryAttributes) case MessageGroupEntry(MessageGroupInfo, [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)], ChatPresentationData) case UnreadEntry(MessageIndex, ChatPresentationData) @@ -49,7 +49,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { case ChatInfoEntry(String, String, TelegramMediaImage?, TelegramMediaFile?, ChatPresentationData) case SearchEntry(PresentationTheme, PresentationStrings) - var stableId: UInt64 { + public var stableId: UInt64 { switch self { case let .MessageEntry(message, _, _, _, _, attributes): let type: UInt64 @@ -75,7 +75,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { } } - var index: MessageIndex { + public var index: MessageIndex { switch self { case let .MessageEntry(message, _, _, _, _, _): return message.index @@ -92,7 +92,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { } } - var firstIndex: MessageIndex { + public var firstIndex: MessageIndex { switch self { case let .MessageEntry(message, _, _, _, _, _): return message.index @@ -109,7 +109,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { } } - static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { + public static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { switch lhs { case let .MessageEntry(lhsMessage, lhsPresentationData, lhsRead, _, lhsSelection, lhsAttributes): switch rhs { @@ -264,7 +264,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { } } - static func <(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { + public static func <(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { let lhsIndex = lhs.index let rhsIndex = rhs.index if lhsIndex == rhsIndex { 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/ChatMessageBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD new file mode 100644 index 0000000000..5b99148dfe --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageBubbleContentNode", + module_name = "ChatMessageBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramUIPreferences", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/ChatMessageBackground", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift new file mode 100644 index 0000000000..ce9ca5971e --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift @@ -0,0 +1,269 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramUIPreferences +import TelegramPresentationData +import AccountContext +import ChatMessageBackground +import ChatControllerInteraction +import ChatHistoryEntry +import ChatMessageItemCommon + +public enum ChatMessageBubbleContentBackgroundHiding { + case never + case emptyWallpaper + case always +} + +public enum ChatMessageBubbleContentAlignment { + case none + case center +} + +public struct ChatMessageBubbleContentProperties { + public let hidesSimpleAuthorHeader: Bool + public let headerSpacing: CGFloat + public let hidesBackground: ChatMessageBubbleContentBackgroundHiding + public let forceFullCorners: Bool + public let forceAlignment: ChatMessageBubbleContentAlignment + public let shareButtonOffset: CGPoint? + public let hidesHeaders: Bool + public let avatarOffset: CGFloat? + + public init( + hidesSimpleAuthorHeader: Bool, + headerSpacing: CGFloat, + hidesBackground: ChatMessageBubbleContentBackgroundHiding, + forceFullCorners: Bool, + forceAlignment: ChatMessageBubbleContentAlignment, + shareButtonOffset: CGPoint? = nil, + hidesHeaders: Bool = false, + avatarOffset: CGFloat? = nil + ) { + self.hidesSimpleAuthorHeader = hidesSimpleAuthorHeader + self.headerSpacing = headerSpacing + self.hidesBackground = hidesBackground + self.forceFullCorners = forceFullCorners + self.forceAlignment = forceAlignment + self.shareButtonOffset = shareButtonOffset + self.hidesHeaders = hidesHeaders + self.avatarOffset = avatarOffset + } +} + +public enum ChatMessageBubbleNoneMergeStatus { + case Incoming + case Outgoing + case None +} + +public enum ChatMessageBubbleMergeStatus { + case None(ChatMessageBubbleNoneMergeStatus) + case Left + case Right + case Both +} + +public enum ChatMessageBubbleRelativePosition { + public enum NeighbourType { + case media + case freeform + } + + public enum NeighbourSpacing { + case `default` + case condensed + case overlap(CGFloat) + } + + case None(ChatMessageBubbleMergeStatus) + case BubbleNeighbour + case Neighbour(Bool, NeighbourType, NeighbourSpacing) +} + +public enum ChatMessageBubbleContentMosaicNeighbor { + case merged + case mergedBubble + case none(tail: Bool) +} + +public struct ChatMessageBubbleContentMosaicPosition { + public let topLeft: ChatMessageBubbleContentMosaicNeighbor + public let topRight: ChatMessageBubbleContentMosaicNeighbor + public let bottomLeft: ChatMessageBubbleContentMosaicNeighbor + public let bottomRight: ChatMessageBubbleContentMosaicNeighbor + + public init(topLeft: ChatMessageBubbleContentMosaicNeighbor, topRight: ChatMessageBubbleContentMosaicNeighbor, bottomLeft: ChatMessageBubbleContentMosaicNeighbor, bottomRight: ChatMessageBubbleContentMosaicNeighbor) { + self.topLeft = topLeft + self.topRight = topRight + self.bottomLeft = bottomLeft + self.bottomRight = bottomRight + } +} + +public enum ChatMessageBubbleContentPosition { + case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) + case mosaic(position: ChatMessageBubbleContentMosaicPosition, wide: Bool) +} + +public enum ChatMessageBubblePreparePosition { + case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) + case mosaic(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) +} + +public enum ChatMessageBubbleContentTapAction { + case none + case url(url: String, concealed: Bool) + case textMention(String) + case peerMention(peerId: PeerId, mention: String, openProfile: Bool) + case botCommand(String) + case hashtag(String?, String) + case instantPage + case wallpaper + case theme + case call(peerId: PeerId, isVideo: Bool) + case openMessage + case timecode(Double, String) + case tooltip(String, ASDisplayNode?, CGRect?) + case bankCard(String) + case ignore + case openPollResults(Data) + case copy(String) + case largeEmoji(String, String?, TelegramMediaFile) + case customEmoji(TelegramMediaFile) +} + +public final class ChatMessageBubbleContentItem { + public let context: AccountContext + public let controllerInteraction: ChatControllerInteraction + public let message: Message + public let topMessage: Message + public let read: Bool + public let chatLocation: ChatLocation + public let presentationData: ChatPresentationData + public let associatedData: ChatMessageItemAssociatedData + public let attributes: ChatMessageEntryAttributes + public let isItemPinned: Bool + public let isItemEdited: Bool + + public init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, topMessage: Message, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool, isItemEdited: Bool) { + self.context = context + self.controllerInteraction = controllerInteraction + self.message = message + self.topMessage = topMessage + self.read = read + self.chatLocation = chatLocation + self.presentationData = presentationData + self.associatedData = associatedData + self.attributes = attributes + self.isItemPinned = isItemPinned + self.isItemEdited = isItemEdited + } +} + +open class ChatMessageBubbleContentNode: ASDisplayNode { + open var supportsMosaic: Bool { + return false + } + + public weak var bubbleBackgroundNode: ChatMessageBackground? + public weak var bubbleBackdropNode: ChatMessageBubbleBackdrop? + + open var visibility: ListViewItemNodeVisibility = .none + + public var item: ChatMessageBubbleContentItem? + + public var updateIsTextSelectionActive: ((Bool) -> Void)? + + open var disablesClipping: Bool { + return false + } + + required public override init() { + super.init() + } + + open func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + preconditionFailure() + } + + open func animateInsertion(_ currentTimestamp: Double, duration: Double) { + } + + open func animateAdded(_ currentTimestamp: Double, duration: Double) { + } + + open func animateRemoved(_ currentTimestamp: Double, duration: Double) { + } + + open func animateInsertionIntoBubble(_ duration: Double) { + } + + open func animateRemovalFromBubble(_ duration: Double, completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + open func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + return nil + } + + open func updateHiddenMedia(_ media: [Media]?) -> Bool { + return false + } + + open func updateSearchTextHighlightState(text: String?, messages: [MessageIndex]?) { + } + + open func updateAutomaticMediaDownloadSettings(_ settings: MediaAutoDownloadSettings) { + } + + open func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { + return nil + } + + open func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + return .none + } + + open func updateTouchesAtPoint(_ point: CGPoint?) { + } + + open func updateHighlightedState(animated: Bool) -> Bool { + return false + } + + open func willUpdateIsExtractedToContextPreview(_ value: Bool) { + } + + open func updateIsExtractedToContextPreview(_ value: Bool) { + } + + open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + } + + open func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + } + + open func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + } + + open func unreadMessageRangeUpdated() { + } + + open func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + return nil + } + + open func targetForStoryTransition(id: StoryId) -> UIView? { + return nil + } + + open func getStatusNode() -> ASDisplayNode? { + return nil + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/BUILD new file mode 100644 index 0000000000..87db0a7555 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/BUILD @@ -0,0 +1,32 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageDateAndStatusNode", + module_name = "ChatMessageDateAndStatusNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramUIPreferences", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/TelegramStringFormatting", + "//submodules/LocalizedPeerData", + "//submodules/Components/ReactionButtonListComponent", + "//submodules/Components/ReactionImageComponent", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift similarity index 98% rename from submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index 45855f26e9..d1da259e64 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -29,13 +29,13 @@ private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) { layer.add(basicAnimation, forKey: "clockFrameAnimation") } -enum ChatMessageDateAndStatusOutgoingType: Equatable { +public enum ChatMessageDateAndStatusOutgoingType: Equatable { case Sent(read: Bool) case Sending case Failed } -enum ChatMessageDateAndStatusType: Equatable { +public enum ChatMessageDateAndStatusType: Equatable { case BubbleIncoming case BubbleOutgoing(ChatMessageDateAndStatusOutgoingType) case ImageIncoming @@ -176,27 +176,27 @@ private final class StatusReactionNode: ASDisplayNode { } } -class ChatMessageDateAndStatusNode: ASDisplayNode { - struct TrailingReactionSettings { - var displayInline: Bool - var preferAdditionalInset: Bool +public class ChatMessageDateAndStatusNode: ASDisplayNode { + public struct TrailingReactionSettings { + public var displayInline: Bool + public var preferAdditionalInset: Bool - init(displayInline: Bool, preferAdditionalInset: Bool) { + public init(displayInline: Bool, preferAdditionalInset: Bool) { self.displayInline = displayInline self.preferAdditionalInset = preferAdditionalInset } } - struct StandaloneReactionSettings { - init() { + public struct StandaloneReactionSettings { + public init() { } } - enum LayoutInput { + public enum LayoutInput { case trailingContent(contentWidth: CGFloat, reactionSettings: TrailingReactionSettings?) case standalone(reactionSettings: StandaloneReactionSettings?) - var displayInlineReactions: Bool { + public var displayInlineReactions: Bool { switch self { case let .trailingContent(_, reactionSettings): if let reactionSettings = reactionSettings { @@ -214,7 +214,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } - struct Arguments { + public struct Arguments { var context: AccountContext var presentationData: ChatPresentationData var edited: Bool @@ -234,7 +234,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var animationCache: AnimationCache var animationRenderer: MultiAnimationRenderer - init( + public init( context: AccountContext, presentationData: ChatPresentationData, edited: Bool, @@ -297,8 +297,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { private var tapGestureRecognizer: UITapGestureRecognizer? - var openReplies: (() -> Void)? - var pressed: (() -> Void)? { + public var openReplies: (() -> Void)? + public var pressed: (() -> Void)? { didSet { if self.pressed != nil { if self.tapGestureRecognizer == nil { @@ -312,10 +312,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } } - var reactionSelected: ((MessageReaction.Reaction) -> Void)? - var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)? + public var reactionSelected: ((MessageReaction.Reaction) -> Void)? + public var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)? - override init() { + override public init() { self.dateNode = TextNode() self.dateNode.isUserInteractionEnabled = false self.dateNode.displaysAsynchronously = false @@ -333,7 +333,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } - func asyncLayout() -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void)) { + public func asyncLayout() -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void)) { let dateLayout = TextNode.asyncLayout(self.dateNode) var checkReadNode = self.checkReadNode @@ -1310,7 +1310,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } - static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)) { + public static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)) { let currentLayout = node?.asyncLayout() return { arguments in let resultNode: ChatMessageDateAndStatusNode @@ -1334,7 +1334,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } - func reactionView(value: MessageReaction.Reaction) -> UIView? { + public func reactionView(value: MessageReaction.Reaction) -> UIView? { for (id, node) in self.reactionNodes { if id == value { return node.iconView @@ -1348,7 +1348,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { return nil } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { for (_, button) in self.reactionButtonsContainer.buttons { if button.view.frame.contains(point) { if let result = button.view.hitTest(self.view.convert(point, to: button.view), with: event) { @@ -1365,6 +1365,6 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } -func shouldDisplayInlineDateReactions(message: Message, isPremium: Bool, forceInline: Bool) -> Bool { +public func shouldDisplayInlineDateReactions(message: Message, isPremium: Bool, forceInline: Bool) -> Bool { return forceInline } diff --git a/submodules/TelegramUI/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift similarity index 91% rename from submodules/TelegramUI/Sources/StringForMessageTimestampStatus.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift index 2769155594..faa4befb03 100644 --- a/submodules/TelegramUI/Sources/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift @@ -7,7 +7,7 @@ import TelegramStringFormatting import LocalizedPeerData import AccountContext -enum MessageTimestampStatusFormat { +public enum MessageTimestampStatusFormat { case regular case minimal } @@ -29,7 +29,7 @@ private func dateStringForDay(strings: PresentationStrings, dateTimeFormat: Pres } } -func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular, associatedData: ChatMessageItemAssociatedData) -> String { +public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular, associatedData: ChatMessageItemAssociatedData) -> String { if let adAttribute = message.adAttribute { switch adAttribute.messageType { case .sponsored: diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/BUILD new file mode 100644 index 0000000000..527d1deaaf --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageItemCommon", + module_name = "ChatMessageItemCommon", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + ], + visibility = [ + "//visibility:public", + ], +) + diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift new file mode 100644 index 0000000000..6c24b99597 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift @@ -0,0 +1,202 @@ +import Foundation +import UIKit +import Display +import Postbox +import TelegramCore + +public struct ChatMessageItemWidthFill { + public var compactInset: CGFloat + public var compactWidthBoundary: CGFloat + public var freeMaximumFillFactor: CGFloat + + public func widthFor(_ width: CGFloat) -> CGFloat { + if width <= self.compactWidthBoundary { + return max(1.0, width - self.compactInset) + } else { + return max(1.0, floor(width * self.freeMaximumFillFactor)) + } + } +} + +public struct ChatMessageItemBubbleLayoutConstants { + public var edgeInset: CGFloat + public var defaultSpacing: CGFloat + public var mergedSpacing: CGFloat + public var maximumWidthFill: ChatMessageItemWidthFill + public var minimumSize: CGSize + public var contentInsets: UIEdgeInsets + public var borderInset: CGFloat + public var strokeInsets: UIEdgeInsets + + public init(edgeInset: CGFloat, defaultSpacing: CGFloat, mergedSpacing: CGFloat, maximumWidthFill: ChatMessageItemWidthFill, minimumSize: CGSize, contentInsets: UIEdgeInsets, borderInset: CGFloat, strokeInsets: UIEdgeInsets) { + self.edgeInset = edgeInset + self.defaultSpacing = defaultSpacing + self.mergedSpacing = mergedSpacing + self.maximumWidthFill = maximumWidthFill + self.minimumSize = minimumSize + self.contentInsets = contentInsets + self.borderInset = borderInset + self.strokeInsets = strokeInsets + } +} + +public struct ChatMessageItemTextLayoutConstants { + public var bubbleInsets: UIEdgeInsets + + public init(bubbleInsets: UIEdgeInsets) { + self.bubbleInsets = bubbleInsets + } +} + +public struct ChatMessageItemImageLayoutConstants { + public var bubbleInsets: UIEdgeInsets + public var statusInsets: UIEdgeInsets + public var defaultCornerRadius: CGFloat + public var mergedCornerRadius: CGFloat + public var contentMergedCornerRadius: CGFloat + public var maxDimensions: CGSize + public var minDimensions: CGSize + + public init(bubbleInsets: UIEdgeInsets, statusInsets: UIEdgeInsets, defaultCornerRadius: CGFloat, mergedCornerRadius: CGFloat, contentMergedCornerRadius: CGFloat, maxDimensions: CGSize, minDimensions: CGSize) { + self.bubbleInsets = bubbleInsets + self.statusInsets = statusInsets + self.defaultCornerRadius = defaultCornerRadius + self.mergedCornerRadius = mergedCornerRadius + self.contentMergedCornerRadius = contentMergedCornerRadius + self.maxDimensions = maxDimensions + self.minDimensions = minDimensions + } +} + +public struct ChatMessageItemVideoLayoutConstants { + public var maxHorizontalHeight: CGFloat + public var maxVerticalHeight: CGFloat + + public init(maxHorizontalHeight: CGFloat, maxVerticalHeight: CGFloat) { + self.maxHorizontalHeight = maxHorizontalHeight + self.maxVerticalHeight = maxVerticalHeight + } +} + +public struct ChatMessageItemInstantVideoConstants { + public var insets: UIEdgeInsets + public var dimensions: CGSize + + public init(insets: UIEdgeInsets, dimensions: CGSize) { + self.insets = insets + self.dimensions = dimensions + } +} + +public struct ChatMessageItemFileLayoutConstants { + public var bubbleInsets: UIEdgeInsets + + public init(bubbleInsets: UIEdgeInsets) { + self.bubbleInsets = bubbleInsets + } +} + +public struct ChatMessageItemWallpaperLayoutConstants { + public var maxTextWidth: CGFloat + + public init(maxTextWidth: CGFloat) { + self.maxTextWidth = maxTextWidth + } +} + +public struct ChatMessageItemLayoutConstants { + public var avatarDiameter: CGFloat + public var timestampHeaderHeight: CGFloat + + public var bubble: ChatMessageItemBubbleLayoutConstants + public var image: ChatMessageItemImageLayoutConstants + public var video: ChatMessageItemVideoLayoutConstants + public var text: ChatMessageItemTextLayoutConstants + public var file: ChatMessageItemFileLayoutConstants + public var instantVideo: ChatMessageItemInstantVideoConstants + public var wallpapers: ChatMessageItemWallpaperLayoutConstants + + public init(avatarDiameter: CGFloat, timestampHeaderHeight: CGFloat, bubble: ChatMessageItemBubbleLayoutConstants, image: ChatMessageItemImageLayoutConstants, video: ChatMessageItemVideoLayoutConstants, text: ChatMessageItemTextLayoutConstants, file: ChatMessageItemFileLayoutConstants, instantVideo: ChatMessageItemInstantVideoConstants, wallpapers: ChatMessageItemWallpaperLayoutConstants) { + self.avatarDiameter = avatarDiameter + self.timestampHeaderHeight = timestampHeaderHeight + self.bubble = bubble + self.image = image + self.video = video + self.text = text + self.file = file + self.instantVideo = instantVideo + self.wallpapers = wallpapers + } + + public static var `default`: ChatMessageItemLayoutConstants { + return self.compact + } + + public static var compact: ChatMessageItemLayoutConstants { + 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)) + let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0)) + let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0) + + return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers) + } + + public static var regular: ChatMessageItemLayoutConstants { + 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)) + let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 240.0, height: 240.0)) + let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0) + + return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers) + } +} + +public func canViewMessageReactionList(message: Message) -> Bool { + var found = false + var canViewList = false + for attribute in message.attributes { + if let attribute = attribute as? ReactionsMessageAttribute { + canViewList = attribute.canViewList + found = true + break + } + } + + if !found { + return false + } + + if let peer = message.peers[message.id.peerId] { + if let channel = peer as? TelegramChannel { + if case .broadcast = channel.info { + return false + } else { + return canViewList + } + } else if let _ = peer as? TelegramGroup { + return canViewList + } else if let _ = peer as? TelegramUser { + return true + } else { + return false + } + } else { + return false + } +} + +public let chatMessagePeerIdColors: [UIColor] = [ + UIColor(rgb: 0xfc5c51), + UIColor(rgb: 0xfa790f), + UIColor(rgb: 0x895dd5), + UIColor(rgb: 0x0fb297), + UIColor(rgb: 0x00c0c2), + UIColor(rgb: 0x3ca5ec), + UIColor(rgb: 0x3d72ed) +] diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/BUILD new file mode 100644 index 0000000000..d4d5e85187 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/BUILD @@ -0,0 +1,33 @@ +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/Postbox", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/LocalizedPeerData", + "//submodules/PhotoResources", + "//submodules/TelegramStringFormatting", + "//submodules/TextFormat", + "//submodules/InvisibleInkDustNode", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift similarity index 82% rename from submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index 3f1699fb42..c8ddf92588 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -15,19 +15,43 @@ import InvisibleInkDustNode import TextNodeWithEntities import AnimationCache import MultiAnimationRenderer +import ChatMessageItemCommon public enum ChatMessageReplyInfoType { case bubble(incoming: Bool) case standalone } +private let quoteIcon: UIImage = { + return UIImage(bundleImageName: "Chat/Message/ReplyQuoteIcon")!.precomposed().withRenderingMode(.alwaysTemplate) +}() + public class ChatMessageReplyInfoNode: ASDisplayNode { + public final class TransitionReplyPanel { + public let titleNode: ASDisplayNode + public let textNode: ASDisplayNode + public let lineNode: ASDisplayNode + public let imageNode: ASDisplayNode + public let relativeSourceRect: CGRect + public let relativeTargetRect: CGRect + + public init(titleNode: ASDisplayNode, textNode: ASDisplayNode, lineNode: ASDisplayNode, imageNode: ASDisplayNode, relativeSourceRect: CGRect, relativeTargetRect: CGRect) { + self.titleNode = titleNode + self.textNode = textNode + self.lineNode = lineNode + self.imageNode = imageNode + self.relativeSourceRect = relativeSourceRect + self.relativeTargetRect = relativeTargetRect + } + } + public class Arguments { public let presentationData: ChatPresentationData public let strings: PresentationStrings 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 +65,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { context: AccountContext, type: ChatMessageReplyInfoType, message: Message?, + quote: EngineMessageReplyQuote?, story: StoryId?, parentMessage: Message, constrainedSize: CGSize, @@ -53,6 +78,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 +96,9 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } } + private let backgroundView: UIImageView + private var quoteIconView: UIImageView? private let contentNode: ASDisplayNode - private let lineNode: ASImageNode private var titleNode: TextNode? private var textNode: TextNodeWithEntities? private var dustNode: InvisibleInkDustNode? @@ -80,24 +107,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 +195,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 +222,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 +248,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 +256,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,26 +349,44 @@ 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) let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) - let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) + var additionalTitleWidth: CGFloat = 0.0 + var maxTextNumberOfLines = 1 + var adjustedConstrainedTextSize = contrainedTextSize + var textCutout: TextNodeCutout? + var textCutoutWidth: CGFloat = 0.0 + if arguments.quote != nil { + additionalTitleWidth += 10.0 + if case .bubble = arguments.type { + maxTextNumberOfLines = 5 + if imageTextInset != 0.0 { + adjustedConstrainedTextSize.width += imageTextInset + textCutout = TextNodeCutout(topLeft: CGSize(width: imageTextInset + 6.0, height: 10.0)) + textCutoutWidth = imageTextInset + 6.0 + } + } + } + + let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: contrainedTextSize.width - additionalTitleWidth, height: contrainedTextSize.height), alignment: .natural, cutout: nil, insets: textInsets)) 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)) + + let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: maxTextNumberOfLines, truncationType: .end, constrainedSize: adjustedConstrainedTextSize, alignment: .natural, lineSpacing: 0.07, 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 +431,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 + additionalTitleWidth - 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 +476,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: 3.0 + UIScreenPixel), size: CGSize(width: imageSide, height: imageSide)) if let updateImageSignal = updateImageSignal { imageNode.setSignal(updateImageSignal) @@ -434,7 +491,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 +547,43 @@ 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 { + if case .standalone = arguments.type { + node.backgroundView.image = PresentationResourcesChat.chatReplyServiceBackgroundTemplateImage(arguments.presentationData.theme.theme) + } else { + node.backgroundView.image = PresentationResourcesChat.chatReplyBackgroundTemplateImage(arguments.presentationData.theme.theme) + } + if node.backgroundView.superview == nil { + node.contentNode.view.insertSubview(node.backgroundView, at: 0) + } + } + + var backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: realSize.width, height: realSize.height + 2.0)) + if case .standalone = arguments.type { + backgroundFrame.size.height -= 1.0 + } + + node.backgroundView.tintColor = mainColor + node.backgroundView.frame = backgroundFrame + + if arguments.quote != nil { + let quoteIconView: UIImageView + if let current = node.quoteIconView { + quoteIconView = current + } else { + quoteIconView = UIImageView(image: quoteIcon) + node.quoteIconView = quoteIconView + node.contentNode.view.addSubview(quoteIconView) + } + quoteIconView.tintColor = mainColor + quoteIconView.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - 4.0 - quoteIcon.size.width, y: backgroundFrame.minY + 4.0), size: quoteIcon.size) + } else { + if let quoteIconView = node.quoteIconView { + node.quoteIconView = nil + quoteIconView.removeFromSuperview() + } + } node.contentNode.frame = CGRect(origin: CGPoint(), size: size) @@ -501,7 +592,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } } - func animateFromInputPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, unclippedTransitionNode: ASDisplayNode? = nil, localRect: CGRect, transition: CombinedTransition) -> CGPoint { + public func animateFromInputPanel(sourceReplyPanel: TransitionReplyPanel, unclippedTransitionNode: ASDisplayNode? = nil, localRect: CGRect, transition: CombinedTransition) -> CGPoint { let sourceParentNode = ASDisplayNode() let sourceParentOffset: CGPoint @@ -588,19 +679,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/Components/Chat/ChatMessageTextBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/BUILD new file mode 100644 index 0000000000..77ed3bbffc --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/BUILD @@ -0,0 +1,43 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageTextBubbleContentNode", + module_name = "ChatMessageTextBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TextFormat", + "//submodules/UrlEscaping", + "//submodules/TelegramUniversalVideoContent", + "//submodules/TextSelectionNode", + "//submodules/InvisibleInkDustNode", + "//submodules/Emoji", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AccountContext", + "//submodules/YuvConversion", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/LottieAnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/MessageQuoteComponent", + "//submodules/TelegramUI/Components/RichTextView", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift similarity index 93% rename from submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index f567ed93fe..b715dda2d6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -20,6 +20,11 @@ import LottieAnimationCache import MultiAnimationRenderer import EmojiTextAttachmentView import TextNodeWithEntities +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ShimmeringLinkNode +import ChatMessageItemCommon +import RichTextView private final class CachedChatMessageText { let text: String @@ -47,7 +52,7 @@ private final class CachedChatMessageText { } } -class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { +public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private let textNode: TextNodeWithEntities private var spoilerTextNode: TextNodeWithEntities? private var dustNode: InvisibleInkDustNode? @@ -62,7 +67,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private var cachedChatMessageText: CachedChatMessageText? - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { if oldValue != self.visibility { switch self.visibility { @@ -80,7 +85,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - required init() { + required public init() { self.textNode = TextNodeWithEntities() self.statusNode = ChatMessageDateAndStatusNode() @@ -117,11 +122,11 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) let spoilerTextLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode) let statusLayout = self.statusNode.asyncLayout() @@ -339,7 +344,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let textFont = item.presentationData.messageFont if let entities = entities { - attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, 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 { @@ -565,22 +570,22 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.textNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.textNode.frame if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(self.dustNode?.isRevealed ?? true) { @@ -605,6 +610,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return .bankCard(bankCard) } else if let pre = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre)] as? String { return .copy(pre) + } else if let code = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Code)] as? String { + return .copy(code) } else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file { return .customEmoji(file) } else { @@ -634,7 +641,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.statusNode.supernode != nil, let result = self.statusNode.hitTest(self.view.convert(point, to: self.statusNode.view), with: event) { return result } @@ -667,7 +674,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { }) } } - override func updateTouchesAtPoint(_ point: CGPoint?) { + override public func updateTouchesAtPoint(_ point: CGPoint?) { if let item = self.item { var rects: [CGRect]? var spoilerRects: [CGRect]? @@ -717,29 +724,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { - if let item = self.item { - let textNodeFrame = self.textNode.textNode.frame - if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { - if let value = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - if let rects = self.textNode.textNode.attributeRects(name: TelegramTextAttributes.URL, at: index), !rects.isEmpty { - var rect = rects[0] - for i in 1 ..< rects.count { - rect = rect.union(rects[i]) - } - var concealed = true - if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { - concealed = !doesUrlMatchText(url: value, text: attributeText, fullText: fullText) - } - return (item.message, .url(self, rect, value, concealed)) - } - } - } - } - return nil - } - - override func updateSearchTextHighlightState(text: String?, messages: [MessageIndex]?) { + override public func updateSearchTextHighlightState(text: String?, messages: [MessageIndex]?) { guard let item = self.item else { return } @@ -768,7 +753,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - override func willUpdateIsExtractedToContextPreview(_ value: Bool) { + override public func willUpdateIsExtractedToContextPreview(_ value: Bool) { if !value { if let textSelectionNode = self.textSelectionNode { self.textSelectionNode = nil @@ -781,7 +766,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - override func updateIsExtractedToContextPreview(_ value: Bool) { + override public func updateIsExtractedToContextPreview(_ value: Bool) { if value { if self.textSelectionNode == nil, let item = self.item, !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected(), let rootNode = item.controllerInteraction.chatControllerNode() { let selectionColor: UIColor @@ -804,7 +789,7 @@ 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 { @@ -816,6 +801,7 @@ 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) @@ -839,18 +825,18 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { if !self.statusNode.isHidden { return self.statusNode.reactionView(value: value) } return nil } - override func getStatusNode() -> ASDisplayNode? { + override public func getStatusNode() -> ASDisplayNode? { return self.statusNode } - func animateFrom(sourceView: UIView, scrollOffset: CGFloat, widthDifference: CGFloat, transition: CombinedTransition) { + public func animateFrom(sourceView: UIView, scrollOffset: CGFloat, widthDifference: CGFloat, transition: CombinedTransition) { self.view.addSubview(sourceView) sourceView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak sourceView] _ in diff --git a/submodules/TelegramUI/Components/Chat/EditableTokenListNode/BUILD b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/BUILD new file mode 100644 index 0000000000..cb19c19e8e --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "EditableTokenListNode", + module_name = "EditableTokenListNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/AvatarNode", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/EditableTokenListNode.swift b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift similarity index 92% rename from submodules/TelegramUI/Sources/EditableTokenListNode.swift rename to submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift index 7e6ae6f971..fd124ea0fa 100644 --- a/submodules/TelegramUI/Sources/EditableTokenListNode.swift +++ b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift @@ -7,16 +7,23 @@ import TelegramPresentationData import AvatarNode import AccountContext -struct EditableTokenListToken { - enum Subject { +public struct EditableTokenListToken { + public enum Subject { case peer(EnginePeer) case category(UIImage?) } - let id: AnyHashable - let title: String - let fixedPosition: Int? - let subject: Subject + public let id: AnyHashable + public let title: String + public let fixedPosition: Int? + public let subject: Subject + + public init(id: AnyHashable, title: String, fixedPosition: Int?, subject: Subject) { + self.id = id + self.title = title + self.fixedPosition = fixedPosition + self.subject = subject + } } private let caretIndicatorImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(rgb: 0x3350ee)) @@ -54,18 +61,18 @@ private func generateRemoveIcon(_ color: UIColor) -> UIImage? { }) } -final class EditableTokenListNodeTheme { - let backgroundColor: UIColor - let separatorColor: UIColor - let placeholderTextColor: UIColor - let primaryTextColor: UIColor - let tokenBackgroundColor: UIColor - let selectedTextColor: UIColor - let selectedBackgroundColor: UIColor - let accentColor: UIColor - let keyboardColor: PresentationThemeKeyboardColor +public final class EditableTokenListNodeTheme { + public let backgroundColor: UIColor + public let separatorColor: UIColor + public let placeholderTextColor: UIColor + public let primaryTextColor: UIColor + public let tokenBackgroundColor: UIColor + public let selectedTextColor: UIColor + public let selectedBackgroundColor: UIColor + public let accentColor: UIColor + public let keyboardColor: PresentationThemeKeyboardColor - init(backgroundColor: UIColor, separatorColor: UIColor, placeholderTextColor: UIColor, primaryTextColor: UIColor, tokenBackgroundColor: UIColor, selectedTextColor: UIColor, selectedBackgroundColor: UIColor, accentColor: UIColor, keyboardColor: PresentationThemeKeyboardColor) { + public init(backgroundColor: UIColor, separatorColor: UIColor, placeholderTextColor: UIColor, primaryTextColor: UIColor, tokenBackgroundColor: UIColor, selectedTextColor: UIColor, selectedBackgroundColor: UIColor, accentColor: UIColor, keyboardColor: PresentationThemeKeyboardColor) { self.backgroundColor = backgroundColor self.separatorColor = separatorColor self.placeholderTextColor = placeholderTextColor @@ -245,7 +252,7 @@ private final class CaretIndicatorNode: ASImageNode { } } -final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { +public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { private let context: AccountContext private let presentationTheme: PresentationTheme @@ -260,11 +267,11 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { private let caretIndicatorNode: CaretIndicatorNode private var selectedTokenId: AnyHashable? - var textUpdated: ((String) -> Void)? - var deleteToken: ((AnyHashable) -> Void)? - var textReturned: (() -> Void)? + public var textUpdated: ((String) -> Void)? + public var deleteToken: ((AnyHashable) -> Void)? + public var textReturned: (() -> Void)? - init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, placeholder: String) { + public init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, placeholder: String) { self.context = context self.presentationTheme = presentationTheme self.theme = theme @@ -326,7 +333,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - func updateLayout(tokens: [EditableTokenListToken], width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + public func updateLayout(tokens: [EditableTokenListToken], width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { let validTokens = Set(tokens.map { $0.id }) for i in (0 ..< self.tokenNodes.count).reversed() { @@ -466,7 +473,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { return nodeHeight } - @objc func textFieldChanged(_ textField: UITextField) { + @objc private func textFieldChanged(_ textField: UITextField) { let text = textField.text ?? "" self.placeholderNode.isHidden = !text.isEmpty self.updateSelectedTokenId(nil) @@ -476,24 +483,24 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { } } - func textFieldShouldReturn(_ textField: UITextField) -> Bool { + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { self.textReturned?() return false } - func textFieldDidBeginEditing(_ textField: UITextField) { + public func textFieldDidBeginEditing(_ textField: UITextField) { /*if self.caretIndicatorNode.supernode == self { self.caretIndicatorNode.removeFromSupernode() }*/ } - func textFieldDidEndEditing(_ textField: UITextField) { + public func textFieldDidEndEditing(_ textField: UITextField) { /*if self.caretIndicatorNode.supernode != self.scrollNode { self.scrollNode.addSubnode(self.caretIndicatorNode) }*/ } - func setText(_ text: String) { + public func setText(_ text: String) { self.textFieldNode.textField.text = text self.textFieldChanged(self.textFieldNode.textField) } diff --git a/submodules/TelegramUI/Components/Chat/MessageQuoteComponent/BUILD b/submodules/TelegramUI/Components/Chat/MessageQuoteComponent/BUILD new file mode 100644 index 0000000000..0145b88d21 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/MessageQuoteComponent/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MessageQuoteComponent", + module_name = "MessageQuoteComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/TelegramCore", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/MessageQuoteComponent/Sources/MessageQuoteComponent.swift b/submodules/TelegramUI/Components/Chat/MessageQuoteComponent/Sources/MessageQuoteComponent.swift new file mode 100644 index 0000000000..b459705bc6 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/MessageQuoteComponent/Sources/MessageQuoteComponent.swift @@ -0,0 +1,67 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import TelegramCore + +private let lineImage: UIImage = { + let radius: CGFloat = 4.0 + return generateImage(CGSize(width: radius, height: radius * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + })!.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(radius)).withRenderingMode(.alwaysTemplate) +}() + +public final class MessageQuoteView: UIView { + public struct Params { + let presentationData: ChatPresentationData + let authorName: String? + let text: String + let entities: [MessageTextEntity] + + public init( + presentationData: ChatPresentationData, + authorName: String?, + text: String, + entities: [MessageTextEntity] + ) { + self.presentationData = presentationData + self.authorName = authorName + self.text = text + self.entities = entities + } + } + + private let lineView: UIImageView + + override private init(frame: CGRect) { + self.lineView = UIImageView() + self.lineView.image = lineImage + + super.init(frame: frame) + + self.addSubview(self.lineView) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public static func asyncLayout(_ view: MessageQuoteView?) -> (Params) -> (CGSize, (CGSize) -> MessageQuoteView) { + return { params in + var minSize = CGSize() + + minSize.height = 100.0 + + return (minSize, { size in + let view = view ?? MessageQuoteView(frame: CGRect()) + + view.lineView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: lineImage.size.width, height: size.height)) + + return view + }) + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/ShimmeringLinkNode/BUILD b/submodules/TelegramUI/Components/Chat/ShimmeringLinkNode/BUILD new file mode 100644 index 0000000000..bdff7f8211 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ShimmeringLinkNode/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ShimmeringLinkNode", + module_name = "ShimmeringLinkNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ShimmerEffect", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ShimmeringLinkNode.swift b/submodules/TelegramUI/Components/Chat/ShimmeringLinkNode/Sources/ShimmeringLinkNode.swift similarity index 89% rename from submodules/TelegramUI/Sources/ShimmeringLinkNode.swift rename to submodules/TelegramUI/Components/Chat/ShimmeringLinkNode/Sources/ShimmeringLinkNode.swift index 790633cf21..c70a1c7953 100644 --- a/submodules/TelegramUI/Sources/ShimmeringLinkNode.swift +++ b/submodules/TelegramUI/Components/Chat/ShimmeringLinkNode/Sources/ShimmeringLinkNode.swift @@ -4,7 +4,7 @@ import AsyncDisplayKit import Display import ShimmerEffect -final class ShimmeringLinkNode: ASDisplayNode { +public final class ShimmeringLinkNode: ASDisplayNode { private let shimmerEffectNode: ShimmerEffectForegroundNode private let borderShimmerEffectNode: ShimmerEffectForegroundNode @@ -12,17 +12,17 @@ final class ShimmeringLinkNode: ASDisplayNode { private let borderMaskNode: ASImageNode private(set) var rects: [CGRect] = [] - var color: UIColor { + public var color: UIColor { didSet { self.backgroundColor = color } } - var innerRadius: CGFloat = 4.0 - var outerRadius: CGFloat = 4.0 - var inset: CGFloat = 2.0 + public var innerRadius: CGFloat = 4.0 + public var outerRadius: CGFloat = 4.0 + public var inset: CGFloat = 2.0 - init(color: UIColor) { + public init(color: UIColor) { self.color = color self.shimmerEffectNode = ShimmerEffectForegroundNode() @@ -46,14 +46,14 @@ final class ShimmeringLinkNode: ASDisplayNode { //self.addSubnode(self.borderShimmerEffectNode) } - override func didLoad() { + override public func didLoad() { super.didLoad() self.shimmerEffectNode.layer.mask = self.maskNode.layer self.borderShimmerEffectNode.layer.mask = self.borderMaskNode.layer } - func updateRects(_ rects: [CGRect], color: UIColor? = nil) { + public func updateRects(_ rects: [CGRect], color: UIColor? = nil) { var updated = false if self.rects != rects { updated = true @@ -86,7 +86,7 @@ final class ShimmeringLinkNode: ASDisplayNode { } } - func updateLayout(_ size: CGSize) { + public func updateLayout(_ size: CGSize) { self.shimmerEffectNode.frame = CGRect(origin: .zero, size: size) self.borderShimmerEffectNode.frame = CGRect(origin: .zero, size: size) 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/ChatEntityKeyboardInputNode/BUILD b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD index fa0c0f90a3..59374b8162 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD @@ -40,6 +40,7 @@ swift_library( "//submodules/StickerPackPreviewUI", "//submodules/TelegramUI/Components/EntityKeyboardGifContent:EntityKeyboardGifContent", "//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView:LegacyMessageInputPanelInputView", + "//submodules/AttachmentTextInputPanelNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 251ae9533d..e65d21aa0a 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -32,6 +32,7 @@ import Pasteboard import StickerPackPreviewUI import EntityKeyboardGifContent import LegacyMessageInputPanelInputView +import AttachmentTextInputPanelNode public final class EmptyInputView: UIView, UIInputViewAudioFeedback { public var enableInputClicksWhenVisible: Bool { @@ -2080,7 +2081,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent } } -public final class EntityInputView: UIInputView, LegacyMessageInputPanelInputView, UIInputViewAudioFeedback { +public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputView, LegacyMessageInputPanelInputView, UIInputViewAudioFeedback { private let context: AccountContext public var insertText: ((NSAttributedString) -> 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/RichTextView/BUILD b/submodules/TelegramUI/Components/RichTextView/BUILD new file mode 100644 index 0000000000..0355bdfea0 --- /dev/null +++ b/submodules/TelegramUI/Components/RichTextView/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "RichTextView", + module_name = "RichTextView", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/RichTextView/Sources/RichTextView.swift b/submodules/TelegramUI/Components/RichTextView/Sources/RichTextView.swift new file mode 100644 index 0000000000..214fcc7e49 --- /dev/null +++ b/submodules/TelegramUI/Components/RichTextView/Sources/RichTextView.swift @@ -0,0 +1,82 @@ +import Foundation +import UIKit + +public final class RichTextView: UIView { + public final class Params: Equatable { + let string: NSAttributedString + let constrainedSize: CGSize + + public init( + string: NSAttributedString, + constrainedSize: CGSize + ) { + self.string = string + self.constrainedSize = constrainedSize + } + + public static func ==(lhs: Params, rhs: Params) -> Bool { + if !lhs.string.isEqual(to: rhs.string) { + return false + } + if lhs.constrainedSize != rhs.constrainedSize { + return false + } + return true + } + } + + public final class LayoutData: Equatable { + init() { + } + + public static func ==(lhs: LayoutData, rhs: LayoutData) -> Bool { + return true + } + } + + public final class AsyncResult { + public let view: () -> RichTextView + public let layoutData: LayoutData + + init(view: @escaping () -> RichTextView, layoutData: LayoutData) { + self.view = view + self.layoutData = layoutData + } + } + + private static func performLayout(params: Params) -> LayoutData { + return LayoutData() + } + + public static func updateAsync(_ view: RichTextView?) -> (Params) -> AsyncResult { + return { params in + let layoutData = performLayout(params: params) + + return AsyncResult( + view: { + let view = view ?? RichTextView(frame: CGRect()) + view.layoutData = layoutData + return view + }, + layoutData: layoutData + ) + } + } + + private var layoutData: LayoutData? + + override public init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func draw(_ rect: CGRect) { + guard let layoutData = self.layoutData else { + return + } + let _ = layoutData + } +} 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..defa0d38f0 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 @@ -5548,6 +5550,12 @@ public final class StoryItemSetContainerComponent: Component { } } + for mediaArea in component.slice.item.storyItem.mediaAreas { + if case let .reaction(_, reaction, _) = mediaArea, case let .custom(fileId) = reaction { + emojiFileIds.append(fileId) + } + } + if !emojiFileIds.isEmpty || hasLinkedStickers, let peerReference = PeerReference(component.slice.peer._asPeer()) { let context = component.context diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 002bcecf3c..6a178e151a 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/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/Contents.json new file mode 100644 index 0000000000..922ad2ecb0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "quotemini.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/quotemini.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/quotemini.pdf new file mode 100644 index 0000000000..15b20f4560 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/quotemini.pdf @@ -0,0 +1,158 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 9.000000 7.000244 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 -0.141113 cm +0.000000 0.000000 0.000000 scn +0.000000 5.141357 m +0.000000 6.245927 0.895431 7.141357 2.000000 7.141357 c +3.104569 7.141357 4.000000 6.245927 4.000000 5.141357 c +4.000000 4.557765 l +4.000000 3.471050 3.746984 2.399258 3.260991 1.427270 c +2.894427 0.694144 l +2.647438 0.200165 2.046765 -0.000059 1.552786 0.246930 c +1.058808 0.493919 0.858584 1.094593 1.105573 1.588571 c +1.472136 2.321697 l +1.605720 2.588866 1.714662 2.866591 1.798144 3.151417 c +0.788369 3.252621 0.000000 4.104923 0.000000 5.141357 c +h +5.000000 5.141357 m +5.000000 6.245927 5.895431 7.141357 7.000000 7.141357 c +8.104569 7.141357 9.000000 6.245927 9.000000 5.141357 c +9.000000 4.557765 l +9.000000 3.471050 8.746984 2.399258 8.260990 1.427270 c +7.894427 0.694144 l +7.647438 0.200165 7.046765 -0.000059 6.552786 0.246930 c +6.058808 0.493919 5.858583 1.094593 6.105573 1.588571 c +6.472136 2.321697 l +6.605721 2.588866 6.714662 2.866591 6.798144 3.151417 c +5.788369 3.252621 5.000000 4.104923 5.000000 5.141357 c +h +f* +n +Q + +endstream +endobj + +2 0 obj + 1077 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 9.000000 7.000244 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 7.000244 m +9.000000 7.000244 l +9.000000 0.000025 l +0.000000 0.000025 l +0.000000 7.000244 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 227 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 9.000000 7.000244 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000001333 00000 n +0000001356 00000 n +0000001829 00000 n +0000001851 00000 n +0000002149 00000 n +0000002251 00000 n +0000002272 00000 n +0000002443 00000 n +0000002517 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +2577 +%%EOF \ No newline at end of file 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 cd34983cc4..e2198870ae 100644 --- a/submodules/TelegramUI/Sources/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/Sources/ChatBotInfoItem.swift @@ -14,6 +14,7 @@ import UniversalMediaPlayer import TelegramUniversalVideoContent import WallpaperBackgroundNode import ChatControllerInteraction +import ChatMessageBubbleContentNode private let messageFont = Font.regular(17.0) private let messageBoldFont = Font.semibold(17.0) @@ -229,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 7c899ff3f1..d5e22e0ad0 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -103,6 +103,7 @@ import ChatContextQuery import PeerReportScreen import PeerSelectionController import SaveToCameraRoll +import ChatMessageDateAndStatusNode #if DEBUG import os.signpost @@ -529,6 +530,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + override public var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? { + return .widthMultiplier(factor: 0.35, min: 16.0, max: 200.0) + } + private var scheduledScrollToMessageId: (MessageId, Double?)? public var purposefulAction: (() -> Void)? @@ -2256,7 +2261,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) @@ -2268,9 +2273,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 { @@ -2307,7 +2312,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())) } @@ -2366,7 +2371,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) @@ -2432,7 +2437,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) } @@ -2442,7 +2447,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) @@ -2858,7 +2863,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) @@ -2867,7 +2872,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) { @@ -3831,7 +3840,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 { @@ -3881,7 +3890,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 } @@ -3963,7 +3972,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 { @@ -8529,7 +8571,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 { @@ -8542,7 +8589,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 { @@ -9361,13 +9408,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) @@ -9376,7 +9423,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 } @@ -10563,7 +10610,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) @@ -10599,7 +10646,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 } @@ -10614,7 +10661,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() } @@ -12044,7 +12091,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 @@ -12170,8 +12217,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 @@ -12190,8 +12241,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 @@ -13819,14 +13873,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) @@ -13884,17 +13938,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) } } @@ -13945,13 +13999,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) @@ -13960,7 +14014,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 @@ -13970,13 +14024,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) @@ -13985,7 +14039,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) @@ -14026,8 +14080,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) } @@ -14037,7 +14091,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() } @@ -14405,7 +14459,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 { @@ -14465,7 +14519,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 { @@ -14482,7 +14536,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) @@ -14831,14 +14885,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) @@ -14881,17 +14935,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) } } @@ -14942,17 +14996,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 @@ -14962,17 +15016,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) @@ -15301,13 +15355,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) @@ -15332,7 +15386,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G correlationId: nil, bubbleUpEmojiOrStickersets: [] ) - strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageId)]) + strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)]) }) } @@ -15502,12 +15556,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 } @@ -15515,11 +15569,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 @@ -15679,19 +15733,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() @@ -15731,17 +15785,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) }) } })) } @@ -15749,17 +15803,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) }) } })) } @@ -15779,17 +15833,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) }) } })) } @@ -15812,8 +15866,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() @@ -15823,7 +15877,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 @@ -15925,10 +15979,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 @@ -15951,7 +16005,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) @@ -16062,12 +16116,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 @@ -16159,12 +16213,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 { @@ -18668,11 +18722,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 }) @@ -18688,7 +18745,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 }) @@ -18714,29 +18776,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/ChatDateSelectionSheet.swift b/submodules/TelegramUI/Sources/ChatDateSelectionSheet.swift deleted file mode 100644 index 658c10305e..0000000000 --- a/submodules/TelegramUI/Sources/ChatDateSelectionSheet.swift +++ /dev/null @@ -1,122 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import UIKit -import SwiftSignalKit -import Photos -import TelegramPresentationData -import UIKitRuntimeUtils - -final class ChatDateSelectionSheet: ActionSheetController { - private let strings: PresentationStrings - - private let _ready = Promise() - override var ready: Promise { - return self._ready - } - - init(presentationData: PresentationData, completion: @escaping (Int32) -> Void) { - self.strings = presentationData.strings - - super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) - - self._ready.set(.single(true)) - - var updatedValue: Int32? - self.setItemGroups([ - ActionSheetItemGroup(items: [ - ChatDateSelectorItem(strings: self.strings, valueChanged: { value in - updatedValue = value - }), - ActionSheetButtonItem(title: self.strings.Common_Search, action: { [weak self] in - self?.dismissAnimated() - if let updatedValue = updatedValue { - completion(updatedValue) - } - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.strings.Common_Cancel, action: { [weak self] in - self?.dismissAnimated() - }), - ]) - ]) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -private final class ChatDateSelectorItem: ActionSheetItem { - let strings: PresentationStrings - - let valueChanged: (Int32) -> Void - - init(strings: PresentationStrings, valueChanged: @escaping (Int32) -> Void) { - self.strings = strings - self.valueChanged = valueChanged - } - - func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return ChatDateSelectorItemNode(theme: theme, strings: self.strings, valueChanged: self.valueChanged) - } - - func updateNode(_ node: ActionSheetItemNode) { - } -} - -private final class ChatDateSelectorItemNode: ActionSheetItemNode { - private let theme: ActionSheetControllerTheme - private let strings: PresentationStrings - - private let pickerView: UIDatePicker - - private let valueChanged: (Int32) -> Void - - private var currentValue: Int32 { - return Int32(self.pickerView.date.timeIntervalSince1970) - } - - init(theme: ActionSheetControllerTheme, strings: PresentationStrings, valueChanged: @escaping (Int32) -> Void) { - self.theme = theme - self.strings = strings - self.valueChanged = valueChanged - - UILabel.setDateLabel(theme.primaryTextColor) - - self.pickerView = UIDatePicker() - self.pickerView.datePickerMode = .countDownTimer - self.pickerView.datePickerMode = .date - self.pickerView.locale = Locale(identifier: strings.baseLanguageCode) - - self.pickerView.minimumDate = Date(timeIntervalSince1970: 1376438400.0) - self.pickerView.maximumDate = Date(timeIntervalSinceNow: 2.0) - - if #available(iOS 13.4, *) { - self.pickerView.preferredDatePickerStyle = .wheels - } - - self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor") - self.pickerView.setValue(theme.primaryTextColor, forKey: "highlightColor") - - super.init(theme: theme) - - self.view.addSubview(self.pickerView) - self.pickerView.addTarget(self, action: #selector(self.pickerChanged), for: .valueChanged) - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 157.0) - - self.pickerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 180.0)) - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } - - @objc func pickerChanged() { - self.valueChanged(self.currentValue) - } -} diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 3c49c2552d..fbb3b17d8a 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -5,6 +5,7 @@ import TemporaryCachedPeerDataManager import Emoji import AccountContext import TelegramPresentationData +import ChatHistoryEntry func chatHistoryEntriesForView( location: ChatLocation, diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index ca7e55eb8e..704a465a0a 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -23,6 +23,7 @@ import ChatPresentationInterfaceState import TelegramNotices import ChatControllerInteraction import TranslateUI +import ChatHistoryEntry extension ChatReplyThreadMessage { var effectiveTopId: MessageId { diff --git a/submodules/TelegramUI/Sources/ChatHoleItem.swift b/submodules/TelegramUI/Sources/ChatHoleItem.swift index 761ec425b4..f4fca112ee 100644 --- a/submodules/TelegramUI/Sources/ChatHoleItem.swift +++ b/submodules/TelegramUI/Sources/ChatHoleItem.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit import Display import SwiftSignalKit import TelegramPresentationData +import ChatMessageItemCommon private let titleFont = UIFont.systemFont(ofSize: 13.0) 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/ChatMessageActionItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift index 19313d7696..c519e92d1d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift @@ -19,6 +19,8 @@ import GalleryUI import WallpaperBackgroundNode import InvisibleInkDustNode import TextNodeWithEntities +import ChatMessageBubbleContentNode +import ChatMessageItemCommon private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId, forForumOverview: Bool) -> NSAttributedString? { return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: EngineMessage(message), accountPeerId: accountPeerId, forChatList: false, forForumOverview: forForumOverview) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index c9642ed116..182ef1fd21 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -29,6 +29,10 @@ import ChatPresentationInterfaceState import TextNodeWithEntities import ChatControllerInteraction import ChatMessageForwardInfoNode +import ChatMessageDateAndStatusNode +import ChatMessageItemCommon +import ChatMessageBubbleContentNode +import ChatMessageReplyInfoNode private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) @@ -517,7 +521,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var setupTimestamp: Double? private func setupNode(item: ChatMessageItem) { self.replyRecognizer?.allowBothDirections = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply - self.view.disablesInteractiveTransitionGestureRecognizer = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + if self.isNodeLoaded { + self.view.disablesInteractiveTransitionGestureRecognizer = !item.context.sharedContext.immediateExperimentalUISettings.unidirectionalSwipeToReply + } guard self.animationNode == nil else { return @@ -1113,7 +1119,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) @@ -1210,7 +1216,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? @@ -1231,6 +1237,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 { @@ -1257,6 +1264,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 { @@ -1291,6 +1299,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), @@ -1635,12 +1644,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) @@ -2865,7 +2875,15 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let replyInfoNode = self.replyInfoNode { let localRect = self.contextSourceNode.contentNode.view.convert(sourceReplyPanel.relativeSourceRect, to: replyInfoNode.view) - let offset = replyInfoNode.animateFromInputPanel(sourceReplyPanel: sourceReplyPanel, localRect: localRect, transition: transition) + let mappedPanel = ChatMessageReplyInfoNode.TransitionReplyPanel( + titleNode: sourceReplyPanel.titleNode, + textNode: sourceReplyPanel.textNode, + lineNode: sourceReplyPanel.lineNode, + imageNode: sourceReplyPanel.imageNode, + relativeSourceRect: sourceReplyPanel.relativeSourceRect, + relativeTargetRect: sourceReplyPanel.relativeTargetRect + ) + let offset = replyInfoNode.animateFromInputPanel(sourceReplyPanel: mappedPanel, localRect: localRect, transition: transition) if let replyBackgroundNode = self.replyBackgroundNode { transition.animatePositionAdditive(layer: replyBackgroundNode.layer, offset: offset) replyBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index d2338430ee..e8fb3b2f91 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -19,6 +19,10 @@ import AnimationCache import MultiAnimationRenderer import ChatControllerInteraction import ShimmerEffect +import ChatMessageDateAndStatusNode +import ChatHistoryEntry +import ChatMessageItemCommon +import ChatMessageBubbleContentNode private let buttonFont = Font.semibold(13.0) @@ -479,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/ChatMessageBubbleContentCalclulateImageCorners.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift index 63bd5c68c5..5e72fd305d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift @@ -2,6 +2,8 @@ import Foundation import UIKit import Display import TelegramPresentationData +import ChatMessageBubbleContentNode +import ChatMessageItemCommon func chatMessageBubbleImageContentCorners(relativeContentPosition position: ChatMessageBubbleContentPosition, normalRadius: CGFloat, mergedRadius: CGFloat, mergedWithAnotherContentRadius: CGFloat, layoutConstants: ChatMessageItemLayoutConstants, chatPresentationData: ChatPresentationData) -> ImageCorners { let topLeftCorner: ImageCorner diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift deleted file mode 100644 index dc5858ffd2..0000000000 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift +++ /dev/null @@ -1,264 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import Postbox -import TelegramCore -import TelegramUIPreferences -import TelegramPresentationData -import AccountContext -import ChatMessageBackground -import ChatControllerInteraction - -enum ChatMessageBubbleContentBackgroundHiding { - case never - case emptyWallpaper - case always -} - -enum ChatMessageBubbleContentAlignment { - case none - case center -} - -struct ChatMessageBubbleContentProperties { - let hidesSimpleAuthorHeader: Bool - let headerSpacing: CGFloat - let hidesBackground: ChatMessageBubbleContentBackgroundHiding - let forceFullCorners: Bool - let forceAlignment: ChatMessageBubbleContentAlignment - let shareButtonOffset: CGPoint? - let hidesHeaders: Bool - let avatarOffset: CGFloat? - - init( - hidesSimpleAuthorHeader: Bool, - headerSpacing: CGFloat, - hidesBackground: ChatMessageBubbleContentBackgroundHiding, - forceFullCorners: Bool, - forceAlignment: ChatMessageBubbleContentAlignment, - shareButtonOffset: CGPoint? = nil, - hidesHeaders: Bool = false, - avatarOffset: CGFloat? = nil - ) { - self.hidesSimpleAuthorHeader = hidesSimpleAuthorHeader - self.headerSpacing = headerSpacing - self.hidesBackground = hidesBackground - self.forceFullCorners = forceFullCorners - self.forceAlignment = forceAlignment - self.shareButtonOffset = shareButtonOffset - self.hidesHeaders = hidesHeaders - self.avatarOffset = avatarOffset - } -} - -enum ChatMessageBubbleNoneMergeStatus { - case Incoming - case Outgoing - case None -} - -enum ChatMessageBubbleMergeStatus { - case None(ChatMessageBubbleNoneMergeStatus) - case Left - case Right - case Both -} - -enum ChatMessageBubbleRelativePosition { - enum NeighbourType { - case media - case freeform - } - - enum NeighbourSpacing { - case `default` - case condensed - case overlap(CGFloat) - } - - case None(ChatMessageBubbleMergeStatus) - case BubbleNeighbour - case Neighbour(Bool, NeighbourType, NeighbourSpacing) -} - -enum ChatMessageBubbleContentMosaicNeighbor { - case merged - case mergedBubble - case none(tail: Bool) -} - -struct ChatMessageBubbleContentMosaicPosition { - let topLeft: ChatMessageBubbleContentMosaicNeighbor - let topRight: ChatMessageBubbleContentMosaicNeighbor - let bottomLeft: ChatMessageBubbleContentMosaicNeighbor - let bottomRight: ChatMessageBubbleContentMosaicNeighbor -} - -enum ChatMessageBubbleContentPosition { - case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) - case mosaic(position: ChatMessageBubbleContentMosaicPosition, wide: Bool) -} - -enum ChatMessageBubblePreparePosition { - case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) - case mosaic(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition) -} - -enum ChatMessageBubbleContentTapAction { - case none - case url(url: String, concealed: Bool) - case textMention(String) - case peerMention(peerId: PeerId, mention: String, openProfile: Bool) - case botCommand(String) - case hashtag(String?, String) - case instantPage - case wallpaper - case theme - case call(peerId: PeerId, isVideo: Bool) - case openMessage - case timecode(Double, String) - case tooltip(String, ASDisplayNode?, CGRect?) - case bankCard(String) - case ignore - case openPollResults(Data) - case copy(String) - case largeEmoji(String, String?, TelegramMediaFile) - case customEmoji(TelegramMediaFile) -} - -final class ChatMessageBubbleContentItem { - let context: AccountContext - let controllerInteraction: ChatControllerInteraction - let message: Message - let topMessage: Message - let read: Bool - let chatLocation: ChatLocation - let presentationData: ChatPresentationData - let associatedData: ChatMessageItemAssociatedData - let attributes: ChatMessageEntryAttributes - let isItemPinned: Bool - let isItemEdited: Bool - - init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, topMessage: Message, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool, isItemEdited: Bool) { - self.context = context - self.controllerInteraction = controllerInteraction - self.message = message - self.topMessage = topMessage - self.read = read - self.chatLocation = chatLocation - self.presentationData = presentationData - self.associatedData = associatedData - self.attributes = attributes - self.isItemPinned = isItemPinned - self.isItemEdited = isItemEdited - } -} - -class ChatMessageBubbleContentNode: ASDisplayNode { - var supportsMosaic: Bool { - return false - } - - weak var bubbleBackgroundNode: ChatMessageBackground? - weak var bubbleBackdropNode: ChatMessageBubbleBackdrop? - - var visibility: ListViewItemNodeVisibility = .none - - var item: ChatMessageBubbleContentItem? - - var updateIsTextSelectionActive: ((Bool) -> Void)? - - var disablesClipping: Bool { - return false - } - - required override init() { - super.init() - } - - func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { - preconditionFailure() - } - - func animateInsertion(_ currentTimestamp: Double, duration: Double) { - } - - func animateAdded(_ currentTimestamp: Double, duration: Double) { - } - - func animateRemoved(_ currentTimestamp: Double, duration: Double) { - } - - func animateInsertionIntoBubble(_ duration: Double) { - } - - func animateRemovalFromBubble(_ duration: Double, completion: @escaping () -> Void) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in - completion() - }) - } - - func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { - return nil - } - - func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { - return nil - } - - func updateHiddenMedia(_ media: [Media]?) -> Bool { - return false - } - - func updateSearchTextHighlightState(text: String?, messages: [MessageIndex]?) { - } - - func updateAutomaticMediaDownloadSettings(_ settings: MediaAutoDownloadSettings) { - } - - func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { - return nil - } - - func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { - return .none - } - - func updateTouchesAtPoint(_ point: CGPoint?) { - } - - func updateHighlightedState(animated: Bool) -> Bool { - return false - } - - func willUpdateIsExtractedToContextPreview(_ value: Bool) { - } - - func updateIsExtractedToContextPreview(_ value: Bool) { - } - - func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { - } - - func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { - } - - func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { - } - - func unreadMessageRangeUpdated() { - } - - func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { - return nil - } - - func targetForStoryTransition(id: StoryId) -> UIView? { - return nil - } - - func getStatusNode() -> ASDisplayNode? { - return nil - } -} diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 4ec3409468..20db4f4dd9 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -30,6 +30,12 @@ import ComponentFlow import EmojiStatusComponent import ChatControllerInteraction import ChatMessageForwardInfoNode +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatHistoryEntry +import ChatMessageTextBubbleContentNode +import ChatMessageItemCommon +import ChatMessageReplyInfoNode enum InternalBubbleTapAction { case action(() -> Void) @@ -352,16 +358,6 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ return (result, needSeparateContainers, needReactions) } -let chatMessagePeerIdColors: [UIColor] = [ - UIColor(rgb: 0xfc5c51), - UIColor(rgb: 0xfa790f), - UIColor(rgb: 0x895dd5), - UIColor(rgb: 0x0fb297), - UIColor(rgb: 0x00c0c2), - UIColor(rgb: 0x3ca5ec), - UIColor(rgb: 0x3d72ed) -] - private enum ContentNodeOperation { case remove(index: Int) case insert(index: Int, node: ChatMessageBubbleContentNode) @@ -921,7 +917,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode func animateReplyPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, transition: CombinedTransition) { if let replyInfoNode = self.replyInfoNode { let localRect = self.mainContextSourceNode.contentNode.view.convert(sourceReplyPanel.relativeSourceRect, to: replyInfoNode.view) - let _ = replyInfoNode.animateFromInputPanel(sourceReplyPanel: sourceReplyPanel, unclippedTransitionNode: self.mainContextSourceNode.contentNode, localRect: localRect, transition: transition) + let mappedPanel = ChatMessageReplyInfoNode.TransitionReplyPanel( + titleNode: sourceReplyPanel.titleNode, + textNode: sourceReplyPanel.textNode, + lineNode: sourceReplyPanel.lineNode, + imageNode: sourceReplyPanel.imageNode, + relativeSourceRect: sourceReplyPanel.relativeSourceRect, + relativeTargetRect: sourceReplyPanel.relativeTargetRect + ) + let _ = replyInfoNode.animateFromInputPanel(sourceReplyPanel: mappedPanel, unclippedTransitionNode: self.mainContextSourceNode.contentNode, localRect: localRect, transition: transition) } } @@ -1175,7 +1179,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)), @@ -1515,6 +1519,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var inlineBotNameString: String? var replyMessage: Message? + var replyQuote: EngineMessageReplyQuote? var replyStory: StoryId? var replyMarkup: ReplyMarkupMessageAttribute? var authorNameColor: UIColor? @@ -1531,6 +1536,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 { @@ -1958,7 +1964,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 }) @@ -2137,9 +2143,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, @@ -2147,18 +2153,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 { @@ -2647,7 +2654,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)]?, @@ -2954,7 +2961,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 { @@ -2968,7 +2976,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) @@ -4156,16 +4164,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return nil } - override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { - for contentNode in self.contentNodes { - let frame = contentNode.frame - if let result = contentNode.peekPreviewContent(at: point.offsetBy(dx: -frame.minX, dy: -frame.minY)) { - return result - } - } - return nil - } - override func updateHiddenMedia() { var hasHiddenMosaicStatus = false var hasHiddenBackground = false diff --git a/submodules/TelegramUI/Sources/ChatMessageCallBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageCallBubbleContentNode.swift index 362b2bfae8..abf91721bf 100644 --- a/submodules/TelegramUI/Sources/ChatMessageCallBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageCallBubbleContentNode.swift @@ -6,6 +6,9 @@ import TelegramCore import Postbox import TelegramPresentationData import AppBundle +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import ChatMessageDateAndStatusNode private let titleFont: UIFont = Font.medium(16.0) private let labelFont: UIFont = Font.regular(13.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift index 91b68dd87a..a4dc2044b1 100644 --- a/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift @@ -9,6 +9,8 @@ import TelegramPresentationData import RadialStatusNode import AnimatedCountLabelNode import AnimatedAvatarSetNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode { private let separatorNode: ASDisplayNode diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift index 15274b2d44..dfce7a137b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift @@ -9,6 +9,9 @@ import TelegramPresentationData import AvatarNode import AccountContext import PhoneNumberFormat +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon private let avatarFont = avatarPlaceholderFont(size: 16.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift index 4036766776..c6e6ad4df0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift @@ -5,6 +5,8 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore +import ChatMessageBubbleContentNode +import ChatMessageItemCommon final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubbleContentNode { private let contentNode: ChatMessageAttachedContentNode diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift index ea50080731..44dbc44d16 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift @@ -5,6 +5,8 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore +import ChatMessageBubbleContentNode +import ChatMessageItemCommon final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContentNode { private let contentNode: ChatMessageAttachedContentNode diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift index 4acb2a4ca1..230ed19c27 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift @@ -5,6 +5,8 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore +import ChatMessageBubbleContentNode +import ChatMessageItemCommon final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleContentNode { private let contentNode: ChatMessageAttachedContentNode diff --git a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift index 39a9a6d089..e6ae6db8f3 100644 --- a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift @@ -8,6 +8,9 @@ import TelegramCore import TelegramUIPreferences import ComponentFlow import AudioTranscriptionButtonComponent +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { let interactiveFileNode: ChatMessageInteractiveFileNode diff --git a/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift index d4c4d55f08..ece75f7847 100644 --- a/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift @@ -5,6 +5,8 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore +import ChatMessageBubbleContentNode +import ChatMessageItemCommon final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { private var game: TelegramMediaGame? diff --git a/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift index 03e4ca2769..c7362ea1dc 100644 --- a/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift @@ -18,6 +18,8 @@ import TelegramAnimatedStickerNode import ChatControllerInteraction import ShimmerEffect import Markdown +import ChatMessageBubbleContentNode +import ChatMessageItemCommon private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false) diff --git a/submodules/TelegramUI/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageGiveawayBubbleContentNode.swift index 1c505b7c78..bfe0ae4051 100644 --- a/submodules/TelegramUI/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -15,6 +15,9 @@ import ShimmerEffect import AnimatedStickerNode import TelegramAnimatedStickerNode import AvatarNode +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon private let titleFont = Font.medium(15.0) private let textFont = Font.regular(13.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoBubbleContentNode.swift index a236a18bac..c9a9e4347f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoBubbleContentNode.swift @@ -8,6 +8,9 @@ import TelegramCore import TelegramUIPreferences import ComponentFlow import AudioTranscriptionButtonComponent +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode { let interactiveFileNode: ChatMessageInteractiveFileNode diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 059a73035c..890e52d475 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -14,6 +14,10 @@ import ContextUI import Markdown import ChatControllerInteraction import ChatMessageForwardInfoNode +import ChatMessageDateAndStatusNode +import ChatMessageItemCommon +import ChatMessageBubbleContentNode +import ChatMessageReplyInfoNode private let nameFont = Font.medium(14.0) @@ -413,7 +417,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? @@ -440,6 +444,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 { @@ -482,6 +487,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 { @@ -497,6 +503,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), @@ -740,12 +747,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 a7c93266b8..9c86bb318d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -29,6 +29,9 @@ import AudioTranscriptionPendingIndicatorComponent import UndoUI import TelegramNotices import ChatControllerInteraction +import ChatMessageDateAndStatusNode +import ChatHistoryEntry +import ChatMessageItemCommon private struct FetchControls { let fetch: (Bool) -> Void @@ -1800,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 ab55076e46..e4559f28f5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -20,6 +20,10 @@ import TelegramNotices import Markdown import TextFormat import ChatMessageForwardInfoNode +import ChatMessageDateAndStatusNode +import ChatMessageItemCommon +import ChatMessageBubbleContentNode +import ChatMessageReplyInfoNode struct ChatMessageInstantVideoItemLayoutResult { let contentSize: CGSize @@ -236,7 +240,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? @@ -313,6 +317,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if !ignoreHeaders { var replyMessage: Message? + var replyQuote: EngineMessageReplyQuote? var replyStory: StoryId? for attribute in item.message.attributes { @@ -342,6 +347,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } else { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } + replyQuote = replyAttribute.quote } else if let attribute = attribute as? ReplyStoryAttribute { replyStory = attribute.storyId } @@ -356,6 +362,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), @@ -976,12 +983,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/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 1c42d3c5cd..576446ac90 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -25,6 +25,9 @@ import ContextUI import InvisibleInkDustNode import ChatControllerInteraction import StoryContainerScreen +import ChatMessageDateAndStatusNode +import ChatHistoryEntry +import ChatMessageItemCommon private struct FetchControls { let fetch: (Bool) -> Void diff --git a/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift index fa24d24139..a899b479c6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift @@ -7,6 +7,8 @@ import SwiftSignalKit import TelegramCore import TelegramUIPreferences import TelegramStringFormatting +import ChatMessageBubbleContentNode +import ChatMessageItemCommon private let titleFont: UIFont = Font.semibold(15.0) private let textFont: UIFont = Font.regular(15.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Sources/ChatMessageItem.swift index 2ca5c503f9..082473893b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItem.swift @@ -11,6 +11,7 @@ import AccountContext import Emoji import PersistentStringHash import ChatControllerInteraction +import ChatHistoryEntry public enum ChatMessageItemContent: Sequence { case message(message: Message, read: Bool, selection: ChatHistoryMessageSelection, attributes: ChatMessageEntryAttributes, location: MessageHistoryEntryLocation?) diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index 83a14c8e01..e0d67f9a06 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -11,104 +11,7 @@ import ChatListUI import TelegramPresentationData import SwiftSignalKit import ChatControllerInteraction - -struct ChatMessageItemWidthFill { - var compactInset: CGFloat - var compactWidthBoundary: CGFloat - var freeMaximumFillFactor: CGFloat - - func widthFor(_ width: CGFloat) -> CGFloat { - if width <= self.compactWidthBoundary { - return max(1.0, width - self.compactInset) - } else { - return max(1.0, floor(width * self.freeMaximumFillFactor)) - } - } -} - -struct ChatMessageItemBubbleLayoutConstants { - var edgeInset: CGFloat - var defaultSpacing: CGFloat - var mergedSpacing: CGFloat - var maximumWidthFill: ChatMessageItemWidthFill - var minimumSize: CGSize - var contentInsets: UIEdgeInsets - var borderInset: CGFloat - var strokeInsets: UIEdgeInsets -} - -struct ChatMessageItemTextLayoutConstants { - var bubbleInsets: UIEdgeInsets -} - -struct ChatMessageItemImageLayoutConstants { - var bubbleInsets: UIEdgeInsets - var statusInsets: UIEdgeInsets - var defaultCornerRadius: CGFloat - var mergedCornerRadius: CGFloat - var contentMergedCornerRadius: CGFloat - var maxDimensions: CGSize - var minDimensions: CGSize -} - -struct ChatMessageItemVideoLayoutConstants { - var maxHorizontalHeight: CGFloat - var maxVerticalHeight: CGFloat -} - -struct ChatMessageItemInstantVideoConstants { - var insets: UIEdgeInsets - var dimensions: CGSize -} - -struct ChatMessageItemFileLayoutConstants { - var bubbleInsets: UIEdgeInsets -} - -struct ChatMessageItemWallpaperLayoutConstants { - var maxTextWidth: CGFloat -} - -struct ChatMessageItemLayoutConstants { - var avatarDiameter: CGFloat - var timestampHeaderHeight: CGFloat - - var bubble: ChatMessageItemBubbleLayoutConstants - var image: ChatMessageItemImageLayoutConstants - var video: ChatMessageItemVideoLayoutConstants - var text: ChatMessageItemTextLayoutConstants - var file: ChatMessageItemFileLayoutConstants - var instantVideo: ChatMessageItemInstantVideoConstants - var wallpapers: ChatMessageItemWallpaperLayoutConstants - - static var `default`: ChatMessageItemLayoutConstants { - return self.compact - } - - fileprivate 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 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)) - let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0)) - let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0) - - return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers) - } - - fileprivate 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 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)) - let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 240.0, height: 240.0)) - let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0) - - return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers) - } -} +import ChatMessageItemCommon func chatMessageItemLayoutConstants(_ constants: (ChatMessageItemLayoutConstants, ChatMessageItemLayoutConstants), params: ListViewItemLayoutParams, presentationData: ChatPresentationData) -> ChatMessageItemLayoutConstants { var result: ChatMessageItemLayoutConstants @@ -122,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 @@ -136,11 +39,6 @@ enum ChatMessageItemBottomNeighbor { case merged(semi: Bool) } -enum ChatMessagePeekPreviewContent { - case media(Media) - case url(ASDisplayNode, CGRect, String, Bool) -} - private let voiceMessageDurationFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() formatter.unitsStyle = .spellOut @@ -790,10 +688,6 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol return nil } - func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { - return nil - } - func updateHiddenMedia() { } diff --git a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift index 167cbb4a05..220564143d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift @@ -10,6 +10,9 @@ import PhotoResources import MediaResources import LocationResources import LiveLocationPositionNode +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon private let titleFont = Font.medium(14.0) private let liveTitleFont = Font.medium(16.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift index 39931215e8..71e31e9705 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift @@ -10,6 +10,9 @@ import TelegramPresentationData import AccountContext import GridMessageSelectionNode import ChatControllerInteraction +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { override var supportsMosaic: Bool { @@ -410,15 +413,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return nil } - override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { - if let message = self.item?.message, let currentMedia = self.media, !message.containsSecretMedia { - if self.interactiveImageNode.frame.contains(point), self.interactiveImageNode.isReadyForInteractivePreview() { - return (message, .media(currentMedia)) - } - } - return nil - } - override func updateHiddenMedia(_ media: [Media]?) -> Bool { var mediaHidden = false diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index 1ebd825c24..de0978ffdd 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -11,6 +11,9 @@ import AccountContext import AvatarNode import TelegramPresentationData import ChatMessageBackground +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon func isPollEffectivelyClosed(message: Message, poll: TelegramMediaPoll) -> Bool { if poll.isClosed { diff --git a/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift index 63db8c989d..8b39e78ad5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift @@ -18,6 +18,8 @@ import UniversalMediaPlayer import TelegramUniversalVideoContent import GalleryUI import Markdown +import ChatMessageBubbleContentNode +import ChatMessageItemCommon class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode { private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index d6b74442ad..94ee3c1bdd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -13,40 +13,8 @@ import ReactionButtonListComponent import AccountContext import WallpaperBackgroundNode import ChatControllerInteraction - -func canViewMessageReactionList(message: Message) -> Bool { - var found = false - var canViewList = false - for attribute in message.attributes { - if let attribute = attribute as? ReactionsMessageAttribute { - canViewList = attribute.canViewList - found = true - break - } - } - - if !found { - return false - } - - if let peer = message.peers[message.id.peerId] { - if let channel = peer as? TelegramChannel { - if case .broadcast = channel.info { - return false - } else { - return canViewList - } - } else if let _ = peer as? TelegramGroup { - return canViewList - } else if let _ = peer as? TelegramUser { - return true - } else { - return false - } - } else { - return false - } -} +import ChatMessageBubbleContentNode +import ChatMessageItemCommon final class MessageReactionButtonsNode: ASDisplayNode { enum DisplayType { diff --git a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift index 437d1b8a93..31ea6b63a4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -7,6 +7,9 @@ import SwiftSignalKit import TelegramCore import TelegramPresentationData import TextFormat +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { private let textNode: TextNode diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index cad1b62e95..61bbc9310b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -15,6 +15,9 @@ import ShimmerEffect import WallpaperBackgroundNode import ChatControllerInteraction import ChatMessageForwardInfoNode +import ChatMessageDateAndStatusNode +import ChatMessageItemCommon +import ChatMessageReplyInfoNode private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) @@ -613,7 +616,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) @@ -636,6 +639,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 { @@ -663,6 +667,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 { @@ -697,6 +702,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), @@ -1049,12 +1055,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) @@ -1841,8 +1848,16 @@ class ChatMessageStickerItemNode: ChatMessageItemView { func animateReplyPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, transition: CombinedTransition) { if let replyInfoNode = self.replyInfoNode { let localRect = self.contextSourceNode.contentNode.view.convert(sourceReplyPanel.relativeSourceRect, to: replyInfoNode.view) + let mappedPanel = ChatMessageReplyInfoNode.TransitionReplyPanel( + titleNode: sourceReplyPanel.titleNode, + textNode: sourceReplyPanel.textNode, + lineNode: sourceReplyPanel.lineNode, + imageNode: sourceReplyPanel.imageNode, + relativeSourceRect: sourceReplyPanel.relativeSourceRect, + relativeTargetRect: sourceReplyPanel.relativeTargetRect + ) - let offset = replyInfoNode.animateFromInputPanel(sourceReplyPanel: sourceReplyPanel, localRect: localRect, transition: transition) + let offset = replyInfoNode.animateFromInputPanel(sourceReplyPanel: mappedPanel, localRect: localRect, transition: transition) if let replyBackgroundNode = self.replyBackgroundNode { transition.animatePositionAdditive(layer: replyBackgroundNode.layer, offset: offset) replyBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) diff --git a/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift index b8fcbd37d0..c04b8c0086 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStoryMentionContentNode.swift @@ -21,6 +21,8 @@ import Markdown import ComponentFlow import AvatarStoryIndicatorComponent import AvatarNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode { private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? diff --git a/submodules/TelegramUI/Sources/ChatMessageUnsupportedBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageUnsupportedBubbleContentNode.swift index 1a3d365060..6ffea446a7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageUnsupportedBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageUnsupportedBubbleContentNode.swift @@ -6,6 +6,8 @@ import AsyncDisplayKit import SwiftSignalKit import TelegramCore import TelegramPresentationData +import ChatMessageBubbleContentNode +import ChatMessageItemCommon final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleContentNode { private var buttonNode: ChatMessageAttachedContentButtonNode diff --git a/submodules/TelegramUI/Sources/ChatMessageWallpaperBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWallpaperBubbleContentNode.swift index 28bf565523..c430d8606d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWallpaperBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWallpaperBubbleContentNode.swift @@ -18,6 +18,8 @@ import Markdown import RadialStatusNode import ComponentFlow import AudioTranscriptionPendingIndicatorComponent +import ChatMessageBubbleContentNode +import ChatMessageItemCommon class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode { private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index 7ec0339055..3ca477a4e4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -13,6 +13,8 @@ import InstantPageUI import UrlHandling import GalleryData import TelegramPresentationData +import ChatMessageBubbleContentNode +import ChatMessageItemCommon private let titleFont: UIFont = Font.semibold(15.0) 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/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift index 71948a35e4..2fbce0e9c5 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift @@ -7,6 +7,7 @@ import TelegramPresentationData import MergeLists import AccountContext import ChatControllerInteraction +import ChatHistoryEntry enum ChatRecentActionsEntryContentIndex: Int32 { case header = 0 diff --git a/submodules/TelegramUI/Sources/ChatReplyCountItem.swift b/submodules/TelegramUI/Sources/ChatReplyCountItem.swift index c8e722a973..855eb10ec8 100644 --- a/submodules/TelegramUI/Sources/ChatReplyCountItem.swift +++ b/submodules/TelegramUI/Sources/ChatReplyCountItem.swift @@ -8,6 +8,7 @@ import TelegramPresentationData import AccountContext import WallpaperBackgroundNode import ChatControllerInteraction +import ChatMessageItemCommon private let titleFont = UIFont.systemFont(ofSize: 13.0) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 6b3f7e1909..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 { @@ -3568,7 +3622,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return ASEditableTextNodeTargetForAction(target: nil) } } - } else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) || action == #selector(self.formatAttributesSpoiler(_:)) { + } else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) || action == #selector(self.formatAttributesSpoiler(_:)) || action == #selector(self.formatAttributesQuote(_:)) { if case .format = self.inputMenu.state { if action == #selector(self.formatAttributesSpoiler(_:)), let selectedRange = self.textInputNode?.selectedRange { var intersectsMonospace = false @@ -3582,6 +3636,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } else { return ASEditableTextNodeTargetForAction(target: nil) } + } else if action == #selector(self.formatAttributesQuote(_:)), let selectedRange = self.textInputNode?.selectedRange { + let _ = selectedRange + return ASEditableTextNodeTargetForAction(target: self) } else if action == #selector(self.formatAttributesMonospace(_:)), let selectedRange = self.textInputNode?.selectedRange { var intersectsSpoiler = false self.inputTextState.inputText.enumerateAttributes(in: selectedRange, options: [], using: { attributes, _, _ in @@ -3607,14 +3664,40 @@ 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 { } else { - var children: [UIAction] = [ + var children: [UIAction] = [] + + //TODO:localize + children.append(UIAction(title: "Quote", image: nil) { [weak self] (action) in + if let strongSelf = self { + strongSelf.formatAttributesQuote(strongSelf) + } + }) + + var hasSpoilers = true + if self.presentationInterfaceState?.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat { + hasSpoilers = false + } + + if hasSpoilers { + children.append(UIAction(title: self.strings?.TextFormat_Spoiler ?? "Spoiler", image: nil) { [weak self] (action) in + if let strongSelf = self { + strongSelf.formatAttributesSpoiler(strongSelf) + } + }) + } + + children.append(contentsOf: [ UIAction(title: self.strings?.TextFormat_Bold ?? "Bold", image: nil) { [weak self] (action) in if let strongSelf = self { strongSelf.formatAttributesBold(strongSelf) @@ -3645,20 +3728,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { strongSelf.formatAttributesUnderline(strongSelf) } } - ] - - var hasSpoilers = true - if self.presentationInterfaceState?.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat { - hasSpoilers = false - } - - if hasSpoilers { - children.append(UIAction(title: self.strings?.TextFormat_Spoiler ?? "Spoiler", image: nil) { [weak self] (action) in - if let strongSelf = self { - strongSelf.formatAttributesSpoiler(strongSelf) - } - }) - } + ] as [UIAction]) let formatMenu = UIMenu(title: self.strings?.TextFormat_Format ?? "Format", image: nil, children: children) actions.insert(formatMenu, at: 3) @@ -3666,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 = "" @@ -3737,6 +3812,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + @objc func formatAttributesQuote(_ sender: Any) { + self.inputMenu.back() + + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.quote), inputMode) + } + } + @objc func formatAttributesSpoiler(_ sender: Any) { self.inputMenu.back() @@ -3756,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}"] @@ -3790,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) @@ -3798,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? @@ -3869,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/ChatUnreadItem.swift b/submodules/TelegramUI/Sources/ChatUnreadItem.swift index ecce438083..87cfa3ffa8 100644 --- a/submodules/TelegramUI/Sources/ChatUnreadItem.swift +++ b/submodules/TelegramUI/Sources/ChatUnreadItem.swift @@ -8,6 +8,7 @@ import TelegramPresentationData import AccountContext import WallpaperBackgroundNode import ChatControllerInteraction +import ChatMessageItemCommon private let titleFont = UIFont.systemFont(ofSize: 13.0) diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index 3b2d204a86..66c579b8d6 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -13,6 +13,7 @@ import AlertUI import PresentationDataUtils import ContactListUI import CounterContollerTitleView +import EditableTokenListNode private func peerTokenTitle(accountPeerId: PeerId, peer: Peer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) -> String { if peer.id == accountPeerId { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index fdfddc9fab..725d1259e5 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -11,6 +11,7 @@ import ContactListUI import ChatListUI import AnimationCache import MultiAnimationRenderer +import EditableTokenListNode private struct SearchResultEntry: Identifiable { let index: Int 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/PreparedChatHistoryViewTransition.swift b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift index d516efebf7..6965180c4d 100644 --- a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift +++ b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift @@ -6,6 +6,7 @@ import Display import MergeLists import AccountContext import ChatControllerInteraction +import ChatHistoryEntry func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, reverse: Bool, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, scrollAnimationCurve: ListViewAnimationCurve?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, cachedDataMessages: [MessageId: Message]?, readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?, flashIndicators: Bool, updatedMessageSelection: Bool, messageTransitionNode: ChatMessageTransitionNode?, allUpdated: Bool) -> ChatHistoryViewTransition { var mergeResult: (deleteIndices: [Int], indicesAndItems: [(Int, ChatHistoryEntry, Int?)], updateIndices: [(Int, ChatHistoryEntry, Int)]) 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 cfdd406fd0..c377a2cc1b 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -43,6 +43,7 @@ import TelegramAccountAuxiliaryMethods import PeerSelectionController import LegacyMessageInputPanel import StatisticsUI +import ChatHistoryEntry private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -1508,7 +1509,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 494c70e470..87e9402dff 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -19,8 +19,10 @@ public struct ChatTextInputAttributes { public static let textUrl = NSAttributedString.Key(rawValue: "Attribute__TextUrl") public static let spoiler = NSAttributedString.Key(rawValue: "Attribute__Spoiler") public static let customEmoji = NSAttributedString.Key(rawValue: "Attribute__CustomEmoji") + public static let code = NSAttributedString.Key(rawValue: "Attribute__Code") + public static let quote = NSAttributedString.Key(rawValue: "Attribute__Blockquote") - public static let allAttributes = [ChatTextInputAttributes.bold, ChatTextInputAttributes.italic, ChatTextInputAttributes.monospace, ChatTextInputAttributes.strikethrough, ChatTextInputAttributes.underline, ChatTextInputAttributes.textMention, ChatTextInputAttributes.textUrl, ChatTextInputAttributes.spoiler, ChatTextInputAttributes.customEmoji] + public static let allAttributes = [ChatTextInputAttributes.bold, ChatTextInputAttributes.italic, ChatTextInputAttributes.monospace, ChatTextInputAttributes.strikethrough, ChatTextInputAttributes.underline, ChatTextInputAttributes.textMention, ChatTextInputAttributes.textUrl, ChatTextInputAttributes.spoiler, ChatTextInputAttributes.customEmoji, ChatTextInputAttributes.code, ChatTextInputAttributes.quote] } public let originalTextAttributeKey = NSAttributedString.Key(rawValue: "Attribute__OriginalText") @@ -115,11 +117,24 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo } else if key == ChatTextInputAttributes.customEmoji { 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) + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.firstLineHeadIndent = 8.0 + paragraphStyle.headIndent = 8.0 + //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] { @@ -134,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 { @@ -193,6 +210,22 @@ public final class ChatTextInputTextUrlAttribute: NSObject { } } +public final class ChatTextInputTextQuoteAttribute: NSObject { + override public init() { + super.init() + } + + override public func isEqual(_ object: Any?) -> Bool { + guard let other = object as? ChatTextInputTextQuoteAttribute else { + return false + } + + let _ = other + + return true + } +} + public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { private enum CodingKeys: String, CodingKey { case interactivelySelectedFromPackId @@ -506,8 +539,137 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed } } -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) +private func quoteRangesEqual(_ lhs: [(NSRange, ChatTextInputTextQuoteAttribute)], _ rhs: [(NSRange, ChatTextInputTextQuoteAttribute)]) -> Bool { + if lhs.count != rhs.count { + return false + } + for i in 0 ..< lhs.count { + if lhs[i].0 != rhs[i].0 || !lhs[i].1.isEqual(rhs[i].1) { + return false + } + } + return true +} + +private func refreshBlockQuotes(text: NSString, initialAttributedText: NSAttributedString, attributedText: NSMutableAttributedString, fullRange: NSRange) { + var quoteRanges: [(NSRange, ChatTextInputTextQuoteAttribute)] = [] + initialAttributedText.enumerateAttribute(ChatTextInputAttributes.quote, in: fullRange, options: [], using: { value, range, _ in + if let value = value as? ChatTextInputTextQuoteAttribute { + quoteRanges.append((range, value)) + } + }) + quoteRanges.sort(by: { $0.0.location < $1.0.location }) + let initialQuoteRanges = quoteRanges + + for i in 0 ..< quoteRanges.count { + let range = quoteRanges[i].0 + + var validLower = range.lowerBound + inner1: for i in range.lowerBound ..< range.upperBound { + if let c = UnicodeScalar(text.character(at: i)) { + if textUrlCharacters.contains(c) { + validLower = i + break inner1 + } + } else { + break inner1 + } + } + var validUpper = range.upperBound + inner2: for i in (validLower ..< range.upperBound).reversed() { + if let c = UnicodeScalar(text.character(at: i)) { + if textUrlCharacters.contains(c) { + validUpper = i + 1 + break inner2 + } + } else { + break inner2 + } + } + + let minLower = (i == 0) ? fullRange.lowerBound : quoteRanges[i - 1].0.upperBound + inner3: for i in (minLower ..< validLower).reversed() { + if let c = UnicodeScalar(text.character(at: i)) { + if textUrlEdgeCharacters.contains(c) { + validLower = i + } else { + break inner3 + } + } else { + break inner3 + } + } + + let maxUpper = (i == quoteRanges.count - 1) ? fullRange.upperBound : quoteRanges[i + 1].0.lowerBound + inner3: for i in validUpper ..< maxUpper { + if let c = UnicodeScalar(text.character(at: i)) { + if textUrlEdgeCharacters.contains(c) { + validUpper = i + 1 + } else { + break inner3 + } + } else { + break inner3 + } + } + + quoteRanges[i] = (NSRange(location: validLower, length: validUpper - validLower), quoteRanges[i].1) + } + + quoteRanges = quoteRanges.filter({ $0.0.length > 0 }) + + while quoteRanges.count > 1 { + var hadReductions = false + outer: for i in 0 ..< quoteRanges.count - 1 { + if quoteRanges[i].1 === quoteRanges[i + 1].1 { + var combine = true + inner: for j in quoteRanges[i].0.upperBound ..< quoteRanges[i + 1].0.lowerBound { + if let c = UnicodeScalar(text.character(at: j)) { + if textUrlCharacters.contains(c) { + } else { + combine = false + break inner + } + } else { + combine = false + break inner + } + } + if combine { + hadReductions = true + quoteRanges[i] = (NSRange(location: quoteRanges[i].0.lowerBound, length: quoteRanges[i + 1].0.upperBound - quoteRanges[i].0.lowerBound), quoteRanges[i].1) + quoteRanges.remove(at: i + 1) + break outer + } + } + } + if !hadReductions { + break + } + } + + if quoteRanges.count > 1 { + outer: for i in (1 ..< quoteRanges.count).reversed() { + for j in 0 ..< i { + if quoteRanges[j].1 === quoteRanges[i].1 { + quoteRanges.remove(at: i) + continue outer + } + } + } + } + + if !quoteRangesEqual(quoteRanges, initialQuoteRanges) { + attributedText.removeAttribute(ChatTextInputAttributes.quote, range: fullRange) + for (range, attribute) in quoteRanges { + let _ = attribute + attributedText.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: range) + } + } +} + +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)?) { @@ -515,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 @@ -534,6 +698,13 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider) + text = resultAttributedText.string as NSString + fullRange = NSRange(location: 0, length: text.length) + attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText)) + refreshBlockQuotes(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange) + + resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider) + if !resultAttributedText.isEqual(to: initialAttributedText) { fullRange = NSRange(location: 0, length: textView.textStorage.length) @@ -546,6 +717,7 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo textView.textStorage.removeAttribute(ChatTextInputAttributes.textUrl, range: fullRange) textView.textStorage.removeAttribute(ChatTextInputAttributes.spoiler, range: fullRange) textView.textStorage.removeAttribute(ChatTextInputAttributes.customEmoji, range: fullRange) + textView.textStorage.removeAttribute(ChatTextInputAttributes.quote, range: fullRange) textView.textStorage.addAttribute(NSAttributedString.Key.font, value: Font.regular(baseFontSize), range: fullRange) textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: primaryTextColor, range: fullRange) @@ -589,6 +761,131 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo } else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute { 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) + 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) + } + } + + 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) } } @@ -615,107 +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) - } - } - } - - 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) - } - } - }) } } @@ -744,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 @@ -752,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 @@ -766,7 +962,7 @@ public func refreshChatTextInputTypingAttributes(_ textNode: ASEditableTextNode, } } } - textNode.textView.typingAttributes = filteredAttributes + textView.typingAttributes = filteredAttributes } private func trimRangesForChatInputText(_ text: NSAttributedString) -> (Int, Int) { @@ -882,7 +1078,7 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu stringOffset -= match.range(at: 2).length + match.range(at: 4).length let substring = string.substring(with: match.range(at: 1)) + text + string.substring(with: match.range(at: 5)) - result.append(NSAttributedString(string: substring, attributes: [ChatTextInputAttributes.monospace: true as NSNumber])) + result.append(NSAttributedString(string: substring, attributes: [ChatTextInputAttributes.code: true as NSNumber])) offsetRanges.append((NSMakeRange(matchIndex + match.range(at: 1).length, text.count), 6)) } } @@ -902,13 +1098,20 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu } else { let text = string.substring(with: pre) - let entity = string.substring(with: match.range(at: 7)) - let substring = string.substring(with: match.range(at: 6)) + text + string.substring(with: match.range(at: 9)) + var entity = string.substring(with: match.range(at: 7)) + var substring = string.substring(with: match.range(at: 6)) + text + string.substring(with: match.range(at: 9)) + + if entity == "`" && substring.hasPrefix("``") && substring.hasSuffix("``") { + entity = "```" + substring = String(substring[substring.index(substring.startIndex, offsetBy: 2) ..< substring.index(substring.endIndex, offsetBy: -2)]) + } let textInputAttribute: NSAttributedString.Key? switch entity { case "`": textInputAttribute = ChatTextInputAttributes.monospace + case "```": + textInputAttribute = ChatTextInputAttributes.code case "**": textInputAttribute = ChatTextInputAttributes.bold case "__": diff --git a/submodules/TextFormat/Sources/GenerateTextEntities.swift b/submodules/TextFormat/Sources/GenerateTextEntities.swift index f30fb928c5..095371186f 100644 --- a/submodules/TextFormat/Sources/GenerateTextEntities.swift +++ b/submodules/TextFormat/Sources/GenerateTextEntities.swift @@ -167,6 +167,10 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Spoiler)) } else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute { entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .CustomEmoji(stickerPack: nil, fileId: value.fileId))) + } else if key == ChatTextInputAttributes.code { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Code)) + } else if key == ChatTextInputAttributes.quote { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .BlockQuote)) } } }) diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index a8cf179e0e..b5ddcce41f 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Postbox import TelegramCore +import Display public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [MessageTextEntity]) -> NSAttributedString { var nsString: NSString? @@ -45,6 +46,8 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M string.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: range) case let .CustomEmoji(_, fileId): string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: range) + case .BlockQuote: + string.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: range) default: break } @@ -52,7 +55,9 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M return string } -public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, 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? let string = NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: baseFont, NSAttributedString.Key.foregroundColor: baseColor]) var skipEntity = false @@ -62,6 +67,8 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti } var fontAttributes: [NSRange: ChatTextFontAttributes] = [:] + var nextBlockId = 0 + var rangeOffset: Int = 0 for i in 0 ..< entities.count { if skipEntity { @@ -197,13 +204,13 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti nsString = text as NSString } string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand), value: nsString!.substring(with: range), range: range) - case .Code, .Pre: + case .Pre: string.addAttribute(NSAttributedString.Key.font, value: fixedFont, range: range) if nsString == nil { nsString = text as NSString } string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre), value: nsString!.substring(with: range), range: range) - case .BlockQuote: + case .BlockQuote, .Code: if let fontAttribute = fontAttributes[range] { fontAttributes[range] = fontAttribute.union(.blockQuote) } else { @@ -211,17 +218,37 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti } let paragraphBreak = "\n" - string.insert(NSAttributedString(string: paragraphBreak), at: range.lowerBound) - let paragraphRange = NSRange(location: range.lowerBound + paragraphBreak.count, length: range.upperBound - range.lowerBound) + var nsString = string.string as NSString + var stringLength = nsString.length - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.headIndent = 10.0 - paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])] - string.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: paragraphRange) + let paragraphRange: NSRange + if range.lowerBound == 0 { + paragraphRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound) + } else if nsString.character(at: range.lowerBound) == 0x0a { + paragraphRange = NSRange(location: range.lowerBound + 1, length: range.upperBound - range.lowerBound - 1) + } else if nsString.character(at: range.lowerBound - 1) == 0x0a { + paragraphRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound) + } else { + string.insert(NSAttributedString(string: paragraphBreak), at: range.lowerBound) + paragraphRange = NSRange(location: range.lowerBound + paragraphBreak.count, length: range.upperBound - range.lowerBound) + } + + string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(id: nextBlockId, title: nil, color: baseQuoteTintColor), range: paragraphRange) + nextBlockId += 1 - string.insert(NSAttributedString(string: paragraphBreak), at: paragraphRange.upperBound) - rangeOffset += paragraphBreak.count + nsString = string.string as NSString + stringLength = nsString.length + + if paragraphRange.upperBound < stringLength { + if nsString.character(at: paragraphRange.upperBound) == 0x0a { + string.replaceCharacters(in: NSMakeRange(paragraphRange.upperBound, 1), with: "") + rangeOffset -= 1 + } + } + + rangeOffset += 0 + //rangeOffset += paragraphBreak.count case .BankCard: string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) if underlineLinks && underlineAllLinks { @@ -268,6 +295,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti func addFont(ranges: [NSRange], fontAttributes: ChatTextFontAttributes) { for range in ranges { var font: UIFont? + if fontAttributes == [.bold, .italic] { font = boldItalicFont } else if fontAttributes == [.bold] { @@ -276,7 +304,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/TextFormat/Sources/TelegramAttributes.swift b/submodules/TextFormat/Sources/TelegramAttributes.swift index ffa8508e85..1f75c378d3 100644 --- a/submodules/TextFormat/Sources/TelegramAttributes.swift +++ b/submodules/TextFormat/Sources/TelegramAttributes.swift @@ -42,4 +42,5 @@ public struct TelegramTextAttributes { public static let BlockQuote = "TelegramBlockQuote" public static let Pre = "TelegramPre" public static let Spoiler = "TelegramSpoiler" + public static let Code = "TelegramCode" } 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 {