From 1ac654e8b213c285dd7fae1dbfa5ba9822454fa4 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 8 Jul 2022 15:09:55 +0200 Subject: [PATCH] [Temp] custom emoji layouts --- .../Source/Base/Transition.swift | 50 +- .../Display/Source/ImmediateTextNode.swift | 8 +- submodules/Display/Source/TextNode.swift | 42 +- .../Sources/StatsMessageItem.swift | 2 +- .../Sources/MessageContentKind.swift | 89 +++- submodules/TelegramUI/BUILD | 1 + .../Sources/EmojiTextAttachmentView.swift | 6 +- .../Sources/EntityKeyboard.swift | 92 +++- .../EntityKeyboardTopPanelComponent.swift | 14 +- .../Sources/GifPagerContentComponent.swift | 19 +- .../Components/TextNodeWithEntities/BUILD | 24 + .../Sources/TextNodeWithEntities.swift | 466 ++++++++++++++++++ .../Sources/ChatControllerNode.swift | 6 +- .../Sources/ChatEntityKeyboardInputNode.swift | 174 ++++++- .../ChatInterfaceStateAccessoryPanels.swift | 4 +- .../ChatMessageAnimatedStickerItemNode.swift | 12 +- .../Sources/ChatMessageBubbleItemNode.swift | 26 +- .../ChatMessageInstantVideoItemNode.swift | 12 +- .../Sources/ChatMessageNotificationItem.swift | 4 +- .../Sources/ChatMessageReplyInfoNode.swift | 150 ++++-- .../Sources/ChatMessageStickerItemNode.swift | 12 +- .../ChatMessageTextBubbleContentNode.swift | 185 ++----- .../ChatPinnedMessageTitlePanelNode.swift | 4 +- .../Sources/EditAccessoryPanelNode.swift | 3 +- .../Sources/ReplyAccessoryPanelNode.swift | 27 +- .../WebpagePreviewAccessoryPanelNode.swift | 6 +- 26 files changed, 1155 insertions(+), 283 deletions(-) create mode 100644 submodules/TelegramUI/Components/TextNodeWithEntities/BUILD create mode 100644 submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index a44933d09d..363a67f497 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -202,20 +202,26 @@ public struct Transition { } switch self.animation { case .none: - view.bounds = CGRect(origin: view.bounds.origin, size: frame.size) - view.layer.position = CGPoint(x: frame.midX, y: frame.midY) + view.frame = frame + //view.bounds = CGRect(origin: view.bounds.origin, size: frame.size) + //view.layer.position = CGPoint(x: frame.midX, y: frame.midY) view.layer.removeAnimation(forKey: "position") view.layer.removeAnimation(forKey: "bounds") completion?(true) case .curve: - let previousPosition = view.layer.presentation()?.position ?? view.center - let previousBounds = view.layer.presentation()?.bounds ?? view.bounds + let previousFrame: CGRect + if (view.layer.animation(forKey: "position") != nil || view.layer.animation(forKey: "bounds") != nil), let presentation = view.layer.presentation() { + previousFrame = presentation.frame + } else { + previousFrame = view.frame + } - view.bounds = CGRect(origin: previousBounds.origin, size: frame.size) - view.center = CGPoint(x: frame.midX, y: frame.midY) + view.frame = frame + //view.bounds = CGRect(origin: previousBounds.origin, size: frame.size) + //view.center = CGPoint(x: frame.midX, y: frame.midY) - self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion) - self.animateBounds(view: view, from: previousBounds, to: view.bounds) + self.animatePosition(view: view, from: CGPoint(x: previousFrame.midX, y: previousFrame.midY), to: CGPoint(x: frame.midX, y: frame.midY), completion: completion) + self.animateBounds(view: view, from: CGRect(origin: view.bounds.origin, size: previousFrame.size), to: CGRect(origin: view.bounds.origin, size: frame.size)) } } @@ -230,7 +236,12 @@ public struct Transition { view.layer.removeAnimation(forKey: "bounds") completion?(true) case .curve: - let previousBounds = view.layer.presentation()?.bounds ?? view.bounds + let previousBounds: CGRect + if view.layer.animation(forKey: "bounds") != nil, let presentation = view.layer.presentation() { + previousBounds = presentation.bounds + } else { + previousBounds = view.layer.bounds + } view.bounds = bounds self.animateBounds(view: view, from: previousBounds, to: view.bounds, completion: completion) @@ -248,7 +259,12 @@ public struct Transition { view.layer.removeAnimation(forKey: "position") completion?(true) case .curve: - let previousPosition = view.layer.presentation()?.position ?? view.center + let previousPosition: CGPoint + if view.layer.animation(forKey: "position") != nil, let presentation = view.layer.presentation() { + previousPosition = presentation.position + } else { + previousPosition = view.layer.position + } view.center = position self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion) @@ -266,7 +282,12 @@ public struct Transition { layer.removeAnimation(forKey: "bounds") completion?(true) case .curve: - let previousBounds = layer.presentation()?.bounds ?? layer.bounds + let previousBounds: CGRect + if layer.animation(forKey: "bounds") != nil, let presentation = layer.presentation() { + previousBounds = presentation.bounds + } else { + previousBounds = layer.bounds + } layer.bounds = bounds self.animateBounds(layer: layer, from: previousBounds, to: layer.bounds, completion: completion) @@ -284,7 +305,12 @@ public struct Transition { layer.removeAnimation(forKey: "position") completion?(true) case .curve: - let previousPosition = layer.presentation()?.position ?? layer.position + let previousPosition: CGPoint + if layer.animation(forKey: "position") != nil, let presentation = layer.presentation() { + previousPosition = presentation.position + } else { + previousPosition = layer.position + } layer.position = position self.animatePosition(layer: layer, from: previousPosition, to: layer.position, completion: completion) diff --git a/submodules/Display/Source/ImmediateTextNode.swift b/submodules/Display/Source/ImmediateTextNode.swift index b7dae91235..53b456c4c6 100644 --- a/submodules/Display/Source/ImmediateTextNode.swift +++ b/submodules/Display/Source/ImmediateTextNode.swift @@ -5,6 +5,12 @@ public struct ImmediateTextNodeLayoutInfo { public let size: CGSize public let truncated: Bool public let numberOfLines: Int + + public init(size: CGSize, truncated: Bool, numberOfLines: Int) { + self.size = size + self.truncated = truncated + self.numberOfLines = numberOfLines + } } public class ImmediateTextNode: TextNode { @@ -125,7 +131,7 @@ public class ImmediateTextNode: TextNode { } } - override public func didLoad() { + override open func didLoad() { super.didLoad() self.updateInteractiveActions() diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 9bc8efeef9..8231bad342 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -148,7 +148,23 @@ public final class TextNodeLayoutArguments { public let textStroke: (UIColor, CGFloat)? public let displaySpoilers: Bool - public init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, minimumNumberOfLines: Int = 0, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, verticalAlignment: TextVerticalAlignment = .top, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), lineColor: UIColor? = nil, textShadowColor: UIColor? = nil, textStroke: (UIColor, CGFloat)? = nil, displaySpoilers: Bool = false) { + public init( + attributedString: NSAttributedString?, + backgroundColor: UIColor? = nil, + minimumNumberOfLines: Int = 0, + maximumNumberOfLines: Int, + truncationType: CTLineTruncationType, + constrainedSize: CGSize, + alignment: NSTextAlignment = .natural, + verticalAlignment: TextVerticalAlignment = .top, + lineSpacing: CGFloat = 0.12, + cutout: TextNodeCutout? = nil, + insets: UIEdgeInsets = UIEdgeInsets(), + lineColor: UIColor? = nil, + textShadowColor: UIColor? = nil, + textStroke: (UIColor, CGFloat)? = nil, + displaySpoilers: Bool = false + ) { self.attributedString = attributedString self.backgroundColor = backgroundColor self.minimumNumberOfLines = minimumNumberOfLines @@ -165,6 +181,26 @@ public final class TextNodeLayoutArguments { self.textStroke = textStroke self.displaySpoilers = displaySpoilers } + + public func withAttributedString(_ attributedString: NSAttributedString?) -> TextNodeLayoutArguments { + return TextNodeLayoutArguments( + attributedString: attributedString, + backgroundColor: self.backgroundColor, + minimumNumberOfLines: self.minimumNumberOfLines, + maximumNumberOfLines: self.maximumNumberOfLines, + truncationType: self.truncationType, + constrainedSize: self.constrainedSize, + alignment: self.alignment, + verticalAlignment: self.verticalAlignment, + lineSpacing: self.lineSpacing, + cutout: self.cutout, + insets: self.insets, + lineColor: self.lineColor, + textShadowColor: self.textShadowColor, + textStroke: self.textStroke, + displaySpoilers: self.displaySpoilers + ) + } } public final class TextNodeLayout: NSObject { @@ -881,7 +917,7 @@ public final class TextAccessibilityOverlayNode: ASDisplayNode { } } -public class TextNode: ASDisplayNode { +open class TextNode: ASDisplayNode { public internal(set) var cachedLayout: TextNodeLayout? override public init() { @@ -892,7 +928,7 @@ public class TextNode: ASDisplayNode { self.clipsToBounds = false } - override public func didLoad() { + override open func didLoad() { super.didLoad() } diff --git a/submodules/StatisticsUI/Sources/StatsMessageItem.swift b/submodules/StatisticsUI/Sources/StatsMessageItem.swift index d9c9471f5a..a3725e2a84 100644 --- a/submodules/StatisticsUI/Sources/StatsMessageItem.swift +++ b/submodules/StatisticsUI/Sources/StatsMessageItem.swift @@ -242,7 +242,7 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode { let presentationData = item.context.sharedContext.currentPresentationData.with { $0 } let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: .firstLast, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId) - var text = !item.message.text.isEmpty ? item.message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0 + var text = !item.message.text.isEmpty ? item.message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0.string text = foldLineBreaks(text) var contentImageMedia: Media? diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index cc00c25f01..49fd545979 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -3,6 +3,7 @@ import TelegramCore import TelegramPresentationData import TelegramUIPreferences import PlatformRestrictionMatching +import TextFormat public enum MessageContentKindKey { case text @@ -26,7 +27,7 @@ public enum MessageContentKindKey { } public enum MessageContentKind: Equatable { - case text(String) + case text(NSAttributedString) case image case video case videoMessage @@ -87,6 +88,40 @@ public enum MessageContentKind: Equatable { } } +public func messageTextWithAttributes(message: EngineMessage) -> NSAttributedString { + var attributedText = NSAttributedString(string: message.text) + + var entities: TextEntitiesMessageAttribute? + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + entities = attribute + break + } + } + if let entities = entities?.entities { + let updatedString = NSMutableAttributedString(attributedString: attributedText) + + for entity in entities.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { + guard case let .CustomEmoji(stickerPack, fileId) = entity.type else { + continue + } + + let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + + let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil) + var updatedAttributes: [NSAttributedString.Key: Any] = currentDict + //updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor + updatedAttributes[ChatTextInputAttributes.customEmoji] = ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId) + + let insertString = NSAttributedString(string: updatedString.attributedSubstring(from: range).string, attributes: updatedAttributes) + updatedString.replaceCharacters(in: range, with: insertString) + } + attributedText = updatedString + } + + return attributedText +} + public func messageContentKind(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> MessageContentKind { for attribute in message.attributes { if let attribute = attribute as? RestrictedContentMessageAttribute { @@ -101,7 +136,7 @@ public func messageContentKind(contentSettings: ContentSettings, message: Engine return kind } } - return .text(message.text) + return .text(messageTextWithAttributes(message: message)) } public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil, strings: PresentationStrings? = nil, nameDisplayOrder: PresentationPersonNameOrder? = nil, dateTimeFormat: PresentationDateTimeFormat? = nil, accountPeerId: EnginePeer.Id? = nil) -> MessageContentKind? { @@ -170,7 +205,7 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil } case .action: if let message = message, let strings = strings, let nameDisplayOrder = nameDisplayOrder, let accountPeerId = accountPeerId { - return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat ?? PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), message: message, accountPeerId: accountPeerId, forChatList: false)?.0 ?? "") + return .text(NSAttributedString(string: plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat ?? PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), message: message, accountPeerId: accountPeerId, forChatList: false)?.0 ?? "")) } else { return nil } @@ -189,59 +224,59 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil } } -public func stringForMediaKind(_ kind: MessageContentKind, strings: PresentationStrings) -> (String, Bool) { +public func stringForMediaKind(_ kind: MessageContentKind, strings: PresentationStrings) -> (NSAttributedString, Bool) { switch kind { case let .text(text): return (foldLineBreaks(text), false) case .image: - return (strings.Message_Photo, true) + return (NSAttributedString(string: strings.Message_Photo), true) case .video: - return (strings.Message_Video, true) + return (NSAttributedString(string: strings.Message_Video), true) case .videoMessage: - return (strings.Message_VideoMessage, true) + return (NSAttributedString(string: strings.Message_VideoMessage), true) case .audioMessage: - return (strings.Message_Audio, true) + return (NSAttributedString(string: strings.Message_Audio), true) case let .sticker(text): if text.isEmpty { - return (strings.Message_Sticker, true) + return (NSAttributedString(string: strings.Message_Sticker), true) } else { - return (strings.Message_StickerText(text).string, true) + return (NSAttributedString(string: strings.Message_StickerText(text).string), true) } case .animation: - return (strings.Message_Animation, true) + return (NSAttributedString(string: strings.Message_Animation), true) case let .file(text): if text.isEmpty { - return (strings.Message_File, true) + return (NSAttributedString(string: strings.Message_File), true) } else { - return (text, true) + return (NSAttributedString(string: text), true) } case .contact: - return (strings.Message_Contact, true) + return (NSAttributedString(string: strings.Message_Contact), true) case let .game(text): - return (text, true) + return (NSAttributedString(string: text), true) case .location: - return (strings.Message_Location, true) + return (NSAttributedString(string: strings.Message_Location), true) case .liveLocation: - return (strings.Message_LiveLocation, true) + return (NSAttributedString(string: strings.Message_LiveLocation), true) case .expiredImage: - return (strings.Message_ImageExpired, true) + return (NSAttributedString(string: strings.Message_ImageExpired), true) case .expiredVideo: - return (strings.Message_VideoExpired, true) + return (NSAttributedString(string: strings.Message_VideoExpired), true) case let .poll(text): - return ("📊 \(text)", false) + return (NSAttributedString(string: "📊 \(text)"), false) case let .restricted(text): - return (text, false) + return (NSAttributedString(string: text), false) case let .dice(emoji): - return (emoji, true) + return (NSAttributedString(string: emoji), true) case let .invoice(text): - return (text, true) + return (NSAttributedString(string: text), true) } } -public func descriptionStringForMessage(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> (String, Bool, Bool) { +public func descriptionStringForMessage(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> (NSAttributedString, Bool, Bool) { let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) { - return (foldLineBreaks(message.text), false, true) + return (foldLineBreaks(messageTextWithAttributes(message: message)), false, true) } let result = stringForMediaKind(contentKind, strings: strings) return (result.0, result.1, false) @@ -263,6 +298,10 @@ public func foldLineBreaks(_ text: String) -> String { return result } +public func foldLineBreaks(_ text: NSAttributedString) -> NSAttributedString { + //TODO:localize + return text +} public func trimToLineCount(_ text: String, lineCount: Int) -> String { if lineCount < 1 { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 7fe179a22d..4d9a0a1245 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -288,6 +288,7 @@ swift_library( "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/ChatInputPanelContainer:ChatInputPanelContainer", + "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC", "//submodules/Media/LocalAudioTranscription:LocalAudioTranscription", diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index eecc39922f..630b33ea0e 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -54,7 +54,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } - public init(context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) { + public init(context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, pointSize: CGSize) { self.context = context self.groupId = groupId self.emoji = emoji @@ -63,7 +63,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.placeholderColor = placeholderColor let scale = min(2.0, UIScreenScale) - self.pointSize = CGSize(width: 24, height: 24) + self.pointSize = pointSize self.pixelSize = CGSize(width: self.pointSize.width * scale, height: self.pointSize.height * scale) super.init() @@ -180,7 +180,7 @@ public final class EmojiTextAttachmentView: UIView { private let contentLayer: InlineStickerItemLayer public init(context: AccountContext, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) { - self.contentLayer = InlineStickerItemLayer(context: context, groupId: "textInputView", attemptSynchronousLoad: true, emoji: emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor) + self.contentLayer = InlineStickerItemLayer(context: context, groupId: "textInputView", attemptSynchronousLoad: true, emoji: emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: 24.0, height: 24.0)) super.init(frame: CGRect()) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 12cba3bc97..27c9e26502 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -41,16 +41,43 @@ public final class EntityKeyboardComponent: Component { } } + public struct GifSearchEmoji: Equatable { + public var emoji: String + public var file: TelegramMediaFile + public var title: String + + public init(emoji: String, file: TelegramMediaFile, title: String) { + self.emoji = emoji + self.file = file + self.title = title + } + + public static func ==(lhs: GifSearchEmoji, rhs: GifSearchEmoji) -> Bool { + if lhs.emoji != rhs.emoji { + return false + } + if lhs.file.fileId != rhs.file.fileId { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + } + public let theme: PresentationTheme public let bottomInset: CGFloat public let emojiContent: EmojiPagerContentComponent public let stickerContent: EmojiPagerContentComponent public let gifContent: GifPagerContentComponent + public let availableGifSearchEmojies: [GifSearchEmoji] public let defaultToEmojiTab: Bool public let externalTopPanelContainer: PagerExternalTopPanelContainer? public let topPanelExtensionUpdated: (CGFloat, Transition) -> Void public let hideInputUpdated: (Bool, Bool, Transition) -> Void public let switchToTextInput: () -> Void + public let switchToGifSubject: (GifPagerContentComponent.Subject) -> Void public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode public let deviceMetrics: DeviceMetrics public let hiddenInputHeight: CGFloat @@ -62,11 +89,13 @@ public final class EntityKeyboardComponent: Component { emojiContent: EmojiPagerContentComponent, stickerContent: EmojiPagerContentComponent, gifContent: GifPagerContentComponent, + availableGifSearchEmojies: [GifSearchEmoji], defaultToEmojiTab: Bool, externalTopPanelContainer: PagerExternalTopPanelContainer?, topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void, hideInputUpdated: @escaping (Bool, Bool, Transition) -> Void, switchToTextInput: @escaping () -> Void, + switchToGifSubject: @escaping (GifPagerContentComponent.Subject) -> Void, makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode, deviceMetrics: DeviceMetrics, hiddenInputHeight: CGFloat, @@ -77,11 +106,13 @@ public final class EntityKeyboardComponent: Component { self.emojiContent = emojiContent self.stickerContent = stickerContent self.gifContent = gifContent + self.availableGifSearchEmojies = availableGifSearchEmojies self.defaultToEmojiTab = defaultToEmojiTab self.externalTopPanelContainer = externalTopPanelContainer self.topPanelExtensionUpdated = topPanelExtensionUpdated self.hideInputUpdated = hideInputUpdated self.switchToTextInput = switchToTextInput + self.switchToGifSubject = switchToGifSubject self.makeSearchContainerNode = makeSearchContainerNode self.deviceMetrics = deviceMetrics self.hiddenInputHeight = hiddenInputHeight @@ -104,6 +135,9 @@ public final class EntityKeyboardComponent: Component { if lhs.gifContent != rhs.gifContent { return false } + if lhs.availableGifSearchEmojies != rhs.availableGifSearchEmojies { + return false + } if lhs.defaultToEmojiTab != rhs.defaultToEmojiTab { return false } @@ -162,26 +196,58 @@ public final class EntityKeyboardComponent: Component { let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>() contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(component.gifContent))) var topGifItems: [EntityKeyboardTopPanelComponent.Item] = [] - topGifItems.removeAll() - /*topGifItems.append(EntityKeyboardTopPanelComponent.Item( + //TODO:localize + topGifItems.append(EntityKeyboardTopPanelComponent.Item( id: "recent", - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Media/RecentTabIcon", - tintColor: component.theme.chat.inputMediaPanel.panelIconColor, - maxSize: CGSize(width: 30.0, height: 30.0)) - ) + content: AnyComponent(EntityKeyboardIconTopPanelComponent( + imageName: "Chat/Input/Media/RecentTabIcon", + theme: component.theme, + title: "Recent", + pressed: { [weak self] in + self?.component?.switchToGifSubject(.recent) + } + )) )) topGifItems.append(EntityKeyboardTopPanelComponent.Item( id: "trending", - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Media/TrendingGifs", - tintColor: component.theme.chat.inputMediaPanel.panelIconColor, - maxSize: CGSize(width: 30.0, height: 30.0)) - ) - ))*/ + content: AnyComponent(EntityKeyboardIconTopPanelComponent( + imageName: "Chat/Input/Media/TrendingGifs", + theme: component.theme, + title: "Trending", + pressed: { [weak self] in + self?.component?.switchToGifSubject(.trending) + } + )) + )) + for emoji in component.availableGifSearchEmojies { + topGifItems.append(EntityKeyboardTopPanelComponent.Item( + id: emoji.emoji, + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: component.stickerContent.context, + file: emoji.file, + animationCache: component.stickerContent.animationCache, + animationRenderer: component.stickerContent.animationRenderer, + theme: component.theme, + title: emoji.title, + pressed: { [weak self] in + self?.component?.switchToGifSubject(.emojiSearch(emoji.emoji)) + } + )) + )) + } + let defaultActiveGifItemId: AnyHashable + switch component.gifContent.subject { + case .recent: + defaultActiveGifItemId = "recent" + case .trending: + defaultActiveGifItemId = "trending" + case let .emojiSearch(value): + defaultActiveGifItemId = AnyHashable(value) + } contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, items: topGifItems, + defaultActiveItemId: defaultActiveGifItemId, activeContentItemIdUpdated: gifsContentItemIdUpdated )))) contentIcons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(BundleIconComponent( diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index daf3de90ef..6a685fcf26 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -364,15 +364,18 @@ final class EntityKeyboardTopPanelComponent: Component { let theme: PresentationTheme let items: [Item] + let defaultActiveItemId: AnyHashable? let activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)> init( theme: PresentationTheme, items: [Item], + defaultActiveItemId: AnyHashable? = nil, activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)> ) { self.theme = theme self.items = items + self.defaultActiveItemId = defaultActiveItemId self.activeContentItemIdUpdated = activeContentItemIdUpdated } @@ -383,6 +386,9 @@ final class EntityKeyboardTopPanelComponent: Component { if lhs.items != rhs.items { return false } + if lhs.defaultActiveItemId != rhs.defaultActiveItemId { + return false + } if lhs.activeContentItemIdUpdated !== rhs.activeContentItemIdUpdated { return false } @@ -613,6 +619,10 @@ final class EntityKeyboardTopPanelComponent: Component { } self.component = component + if let defaultActiveItemId = component.defaultActiveItemId { + self.activeContentItemId = defaultActiveItemId + } + let panelEnvironment = environment[EntityKeyboardTopContainerPanelEnvironment.self].value self.environment = panelEnvironment @@ -639,8 +649,8 @@ final class EntityKeyboardTopPanelComponent: Component { let previousItemFrame = previousItemLayout.containerFrame(at: previousVisibleRange.minIndex) let updatedItemFrame = itemLayout.containerFrame(at: previousVisibleRange.minIndex) - let previousDistanceToItemFraction = (previousItemFrame.minX - self.scrollView.bounds.minX) / previousItemFrame.width - let newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - floor(previousDistanceToItemFraction * updatedItemFrame.width), y: 0.0), size: availableSize) + let previousDistanceToItem = (previousItemFrame.minX - self.scrollView.bounds.minX)// / previousItemFrame.width + let newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - previousDistanceToItem/* * updatedItemFrame.width)*/, y: 0.0), size: availableSize) updatedBounds = newBounds var updatedVisibleBounds = newBounds diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index 02936915b6..c57bb93a13 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -117,6 +117,12 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer { public final class GifPagerContentComponent: Component { public typealias EnvironmentType = (EntityKeyboardChildEnvironment, PagerComponentChildEnvironment) + public enum Subject: Equatable { + case recent + case trending + case emojiSearch(String) + } + public final class InputInteraction { public let performItemAction: (Item, UIView, CGRect) -> Void @@ -148,15 +154,18 @@ public final class GifPagerContentComponent: Component { public let context: AccountContext public let inputInteraction: InputInteraction + public let subject: Subject public let items: [Item] public init( context: AccountContext, inputInteraction: InputInteraction, + subject: Subject, items: [Item] ) { self.context = context self.inputInteraction = inputInteraction + self.subject = subject self.items = items } @@ -167,6 +176,9 @@ public final class GifPagerContentComponent: Component { if lhs.inputInteraction !== rhs.inputInteraction { return false } + if lhs.subject != rhs.subject { + return false + } if lhs.items != rhs.items { return false } @@ -363,7 +375,10 @@ public final class GifPagerContentComponent: Component { } } - private let scrollView: UIScrollView + private final class ContentScrollView: UIScrollView, PagerExpandableScrollView { + } + + private let scrollView: ContentScrollView private var visibleItemLayers: [MediaId: ItemLayer] = [:] private var ignoreScrolling: Bool = false @@ -374,7 +389,7 @@ public final class GifPagerContentComponent: Component { private var itemLayout: ItemLayout? override init(frame: CGRect) { - self.scrollView = UIScrollView() + self.scrollView = ContentScrollView() super.init(frame: frame) diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/BUILD b/submodules/TelegramUI/Components/TextNodeWithEntities/BUILD new file mode 100644 index 0000000000..402cbbd90a --- /dev/null +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TextNodeWithEntities", + module_name = "TextNodeWithEntities", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/TextFormat:TextFormat", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift new file mode 100644 index 0000000000..f45bd8eed8 --- /dev/null +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -0,0 +1,466 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import EmojiTextAttachmentView +import TextFormat +import AccountContext +import AnimationCache +import MultiAnimationRenderer + +private final class InlineStickerItem: Hashable { + let emoji: ChatTextInputTextCustomEmojiAttribute + let fontSize: CGFloat + + init(emoji: ChatTextInputTextCustomEmojiAttribute, fontSize: CGFloat) { + self.emoji = emoji + self.fontSize = fontSize + } + + func hash(into hasher: inout Hasher) { + hasher.combine(emoji.fileId) + hasher.combine(self.fontSize) + } + + static func ==(lhs: InlineStickerItem, rhs: InlineStickerItem) -> Bool { + if lhs.emoji.fileId != rhs.emoji.fileId { + return false + } + if lhs.fontSize != rhs.fontSize { + return false + } + return true + } +} + +public final class TextNodeWithEntities { + public final class Arguments { + public let context: AccountContext + public let cache: AnimationCache + public let renderer: MultiAnimationRenderer + public let placeholderColor: UIColor + + public init( + context: AccountContext, + cache: AnimationCache, + renderer: MultiAnimationRenderer, + placeholderColor: UIColor + ) { + self.context = context + self.cache = cache + self.renderer = renderer + self.placeholderColor = placeholderColor + } + } + + public let textNode: TextNode + private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:] + + public var visibilityRect: CGRect? { + didSet { + if !self.inlineStickerItemLayers.isEmpty && oldValue != self.visibilityRect { + for (_, itemLayer) in self.inlineStickerItemLayers { + let isItemVisible: Bool + if let visibilityRect = self.visibilityRect { + if itemLayer.frame.intersects(visibilityRect) { + isItemVisible = true + } else { + isItemVisible = false + } + } else { + isItemVisible = false + } + itemLayer.isVisibleForAnimations = isItemVisible + } + } + } + } + + public init() { + self.textNode = TextNode() + } + + private init(textNode: TextNode) { + self.textNode = textNode + } + + public static func asyncLayout(_ maybeNode: TextNodeWithEntities?) -> (TextNodeLayoutArguments) -> (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities) { + let makeLayout = TextNode.asyncLayout(maybeNode?.textNode) + return { [weak maybeNode] arguments in + var updatedString: NSAttributedString? + if let sourceString = arguments.attributedString { + let string = NSMutableAttributedString(attributedString: sourceString) + + let fullRange = NSRange(location: 0, length: string.length) + string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, _ in + if let value = value as? ChatTextInputTextCustomEmojiAttribute { + if let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont { + string.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: value.stickerPack, fileId: value.fileId), fontSize: font.pointSize), range: range) + } + } + }) + + updatedString = string + } + + let (layout, apply) = makeLayout(arguments.withAttributedString(updatedString)) + return (layout, { applyArguments in + let result = apply() + + if let maybeNode = maybeNode { + if let applyArguments = applyArguments { + maybeNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor) + } + + return maybeNode + } else { + let resultNode = TextNodeWithEntities(textNode: result) + + if let applyArguments = applyArguments { + resultNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor) + } + + return resultNode + } + }) + } + } + + private func isItemVisible(itemRect: CGRect) -> Bool { + if let visibilityRect = self.visibilityRect { + return itemRect.intersects(visibilityRect) + } else { + return false + } + } + + private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor) { + var nextIndexById: [Int64: Int] = [:] + var validIds: [InlineStickerItemLayer.Key] = [] + + if let textLayout = textLayout { + for item in textLayout.embeddedItems { + if let stickerItem = item.value as? InlineStickerItem { + let index: Int + if let currentNext = nextIndexById[stickerItem.emoji.fileId] { + index = currentNext + } else { + index = 0 + } + nextIndexById[stickerItem.emoji.fileId] = index + 1 + let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index) + validIds.append(id) + + let itemSize = floor(stickerItem.fontSize * 24.0 / 17.0) + + let itemFrame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left, dy: textLayout.insets.top + 0.0).center, size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0) + + let itemLayer: InlineStickerItemLayer + if let current = self.inlineStickerItemLayers[id] { + itemLayer = current + } else { + itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, emoji: stickerItem.emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize)) + self.inlineStickerItemLayers[id] = itemLayer + self.textNode.layer.addSublayer(itemLayer) + + itemLayer.isVisibleForAnimations = self.isItemVisible(itemRect: itemFrame) + } + + itemLayer.frame = itemFrame + } + } + } + + var removeKeys: [InlineStickerItemLayer.Key] = [] + for (key, itemLayer) in self.inlineStickerItemLayers { + if !validIds.contains(key) { + removeKeys.append(key) + itemLayer.removeFromSuperlayer() + } + } + for key in removeKeys { + self.inlineStickerItemLayers.removeValue(forKey: key) + } + } +} + +public class ImmediateTextNodeWithEntities: TextNode { + public var attributedText: NSAttributedString? + public var textAlignment: NSTextAlignment = .natural + public var verticalAlignment: TextVerticalAlignment = .top + public var truncationType: CTLineTruncationType = .end + public var maximumNumberOfLines: Int = 1 + public var lineSpacing: CGFloat = 0.0 + public var insets: UIEdgeInsets = UIEdgeInsets() + public var textShadowColor: UIColor? + public var textStroke: (UIColor, CGFloat)? + public var cutout: TextNodeCutout? + public var displaySpoilers = false + + public var arguments: TextNodeWithEntities.Arguments? + + private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:] + + public var visibility: Bool = false { + didSet { + if !self.inlineStickerItemLayers.isEmpty && oldValue != self.visibility { + for (_, itemLayer) in self.inlineStickerItemLayers { + let isItemVisible: Bool = self.visibility + itemLayer.isVisibleForAnimations = isItemVisible + } + } + } + } + + public var truncationMode: NSLineBreakMode { + get { + switch self.truncationType { + case .start: + return .byTruncatingHead + case .middle: + return .byTruncatingMiddle + case .end: + return .byTruncatingTail + @unknown default: + return .byTruncatingTail + } + } set(value) { + switch value { + case .byTruncatingHead: + self.truncationType = .start + case .byTruncatingMiddle: + self.truncationType = .middle + case .byTruncatingTail: + self.truncationType = .end + default: + self.truncationType = .end + } + } + } + + private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? + private var linkHighlightingNode: LinkHighlightingNode? + + public var linkHighlightColor: UIColor? + + public var trailingLineWidth: CGFloat? + + var constrainedSize: CGSize? + + public var highlightAttributeAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? { + didSet { + if self.isNodeLoaded { + self.updateInteractiveActions() + } + } + } + + public var tapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)? + public var longTapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)? + + private func processedAttributedText() -> NSAttributedString? { + var updatedString: NSAttributedString? + if let sourceString = self.attributedText { + let string = NSMutableAttributedString(attributedString: sourceString) + + let fullRange = NSRange(location: 0, length: string.length) + string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, _ in + if let value = value as? ChatTextInputTextCustomEmojiAttribute { + if let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont { + string.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: value.stickerPack, fileId: value.fileId), fontSize: font.pointSize), range: range) + } + } + }) + + updatedString = string + } + return updatedString + } + + public func updateLayout(_ constrainedSize: CGSize) -> CGSize { + self.constrainedSize = constrainedSize + + let makeLayout = TextNode.asyncLayout(self) + let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.processedAttributedText(), backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, textShadowColor: self.textShadowColor, textStroke: self.textStroke, displaySpoilers: self.displaySpoilers)) + + let _ = apply() + + if let arguments = self.arguments { + self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor) + } + + if layout.numberOfLines > 1 { + self.trailingLineWidth = layout.trailingLineWidth + } else { + self.trailingLineWidth = nil + } + return layout.size + } + + private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor) { + var nextIndexById: [Int64: Int] = [:] + var validIds: [InlineStickerItemLayer.Key] = [] + + if let textLayout = textLayout { + for item in textLayout.embeddedItems { + if let stickerItem = item.value as? InlineStickerItem { + let index: Int + if let currentNext = nextIndexById[stickerItem.emoji.fileId] { + index = currentNext + } else { + index = 0 + } + nextIndexById[stickerItem.emoji.fileId] = index + 1 + let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index) + validIds.append(id) + + let itemSize = floor(stickerItem.fontSize * 24.0 / 17.0) + + let itemFrame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left, dy: textLayout.insets.top + 0.0).center, size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0) + + let itemLayer: InlineStickerItemLayer + if let current = self.inlineStickerItemLayers[id] { + itemLayer = current + } else { + itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, emoji: stickerItem.emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize)) + self.inlineStickerItemLayers[id] = itemLayer + self.layer.addSublayer(itemLayer) + + itemLayer.isVisibleForAnimations = self.visibility + } + + itemLayer.frame = itemFrame + } + } + } + + var removeKeys: [InlineStickerItemLayer.Key] = [] + for (key, itemLayer) in self.inlineStickerItemLayers { + if !validIds.contains(key) { + removeKeys.append(key) + itemLayer.removeFromSuperlayer() + } + } + for key in removeKeys { + self.inlineStickerItemLayers.removeValue(forKey: key) + } + } + + public func updateLayoutInfo(_ constrainedSize: CGSize) -> ImmediateTextNodeLayoutInfo { + self.constrainedSize = constrainedSize + + let makeLayout = TextNode.asyncLayout(self) + let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.processedAttributedText(), backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, displaySpoilers: self.displaySpoilers)) + + let _ = apply() + + if let arguments = self.arguments { + self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor) + } + + return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated, numberOfLines: layout.numberOfLines) + } + + public func updateLayoutFullInfo(_ constrainedSize: CGSize) -> TextNodeLayout { + self.constrainedSize = constrainedSize + + let makeLayout = TextNode.asyncLayout(self) + let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.processedAttributedText(), backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, displaySpoilers: self.displaySpoilers)) + + let _ = apply() + + if let arguments = self.arguments { + self.updateInlineStickers(context: arguments.context, cache: arguments.cache, renderer: arguments.renderer, textLayout: layout, placeholderColor: arguments.placeholderColor) + } + + return layout + } + + public func redrawIfPossible() { + if let constrainedSize = self.constrainedSize { + let _ = self.updateLayout(constrainedSize) + } + } + + override public func didLoad() { + super.didLoad() + + self.updateInteractiveActions() + } + + private func updateInteractiveActions() { + if self.highlightAttributeAction != nil { + if self.tapRecognizer == nil { + let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapAction(_:))) + tapRecognizer.highlight = { [weak self] point in + if let strongSelf = self { + var rects: [CGRect]? + if let point = point { + if let (index, attributes) = strongSelf.attributesAtPoint(CGPoint(x: point.x, y: point.y)) { + if let selectedAttribute = strongSelf.highlightAttributeAction?(attributes) { + let initialRects = strongSelf.lineAndAttributeRects(name: selectedAttribute.rawValue, at: index) + if let initialRects = initialRects, case .center = strongSelf.textAlignment { + var mappedRects: [CGRect] = [] + for i in 0 ..< initialRects.count { + let lineRect = initialRects[i].0 + var itemRect = initialRects[i].1 + itemRect.origin.x = floor((strongSelf.bounds.size.width - lineRect.width) / 2.0) + itemRect.origin.x + mappedRects.append(itemRect) + } + rects = mappedRects + } else { + rects = strongSelf.attributeRects(name: selectedAttribute.rawValue, at: index) + } + } + } + } + + if let rects = rects { + let linkHighlightingNode: LinkHighlightingNode + if let current = strongSelf.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: strongSelf.linkHighlightColor ?? .clear) + strongSelf.linkHighlightingNode = linkHighlightingNode + strongSelf.addSubnode(linkHighlightingNode) + } + linkHighlightingNode.frame = strongSelf.bounds + linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) }) + } else if let linkHighlightingNode = strongSelf.linkHighlightingNode { + strongSelf.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + } + self.view.addGestureRecognizer(tapRecognizer) + } + } else if let tapRecognizer = self.tapRecognizer { + self.tapRecognizer = nil + self.view.removeGestureRecognizer(tapRecognizer) + } + } + + @objc private func tapAction(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + if let (index, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) { + self.tapAttributeAction?(attributes, index) + } + case .longTap: + if let (index, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) { + self.longTapAttributeAction?(attributes, index) + } + default: + break + } + } + default: + break + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 34a5d34f2a..db2c38c78f 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -918,7 +918,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { wasDraggingKeyboard = true } var wasDraggingInputNode = false - if let derivedLayoutState = self.derivedLayoutState, let inputNodeHeight = derivedLayoutState.inputNodeHeight, !inputNodeHeight.isZero, let upperInputPositionBound = derivedLayoutState.upperInputPositionBound { + if let derivedLayoutState = self.derivedLayoutState, let inputNodeHeight = derivedLayoutState.inputNodeHeight, !inputNodeHeight.isZero, let upperInputPositionBound = derivedLayoutState.upperInputPositionBound { let normalizedHeight = max(0.0, layout.size.height - upperInputPositionBound) if normalizedHeight < inputNodeHeight { wasDraggingInputNode = true @@ -1113,7 +1113,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var accessoryPanelSize: CGSize? var immediatelyLayoutAccessoryPanelAndAnimateAppearance = false - if let accessoryPanelNode = accessoryPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.accessoryPanelNode, interfaceInteraction: self.interfaceInteraction) { + if let accessoryPanelNode = accessoryPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.accessoryPanelNode, chatControllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction) { accessoryPanelSize = accessoryPanelNode.measure(CGSize(width: layout.size.width, height: layout.size.height)) accessoryPanelNode.updateState(size: layout.size, inset: layout.safeInsets.left, interfaceState: self.chatPresentationInterfaceState) @@ -2679,7 +2679,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let keyboardGestureBeginLocation = location let accessoryHeight = self.getWindowInputAccessoryHeight() - if let inputHeight = derivedLayoutState.inputNodeHeight, !inputHeight.isZero, keyboardGestureBeginLocation.y < validLayout.size.height - inputHeight - accessoryHeight { + if let inputHeight = derivedLayoutState.inputNodeHeight, !inputHeight.isZero, keyboardGestureBeginLocation.y < validLayout.size.height - inputHeight - accessoryHeight, !self.inputPanelContainerNode.stableIsExpanded { var enableGesture = true if let view = self.view.hitTest(location, with: nil) { if doesViewTreeDisableInteractiveTransitionGestureRecognizer(view) { diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index f5a5170437..9ea40f893a 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -18,18 +18,21 @@ import PagerComponent final class ChatEntityKeyboardInputNode: ChatInputNode { struct InputData: Equatable { - let emoji: EmojiPagerContentComponent - let stickers: EmojiPagerContentComponent - let gifs: GifPagerContentComponent + var emoji: EmojiPagerContentComponent + var stickers: EmojiPagerContentComponent + var gifs: GifPagerContentComponent + var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] init( emoji: EmojiPagerContentComponent, stickers: EmojiPagerContentComponent, - gifs: GifPagerContentComponent + gifs: GifPagerContentComponent, + availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] ) { self.emoji = emoji self.stickers = stickers self.gifs = gifs + self.availableGifSearchEmojies = availableGifSearchEmojies } } @@ -419,7 +422,39 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { ) } - let gifItems: Signal = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) + let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()) + |> map { appConfiguration -> [String] in + let defaultReactions: [String] = ["👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒"] + + guard let data = appConfiguration.data, let emojis = data["gif_search_emojies"] as? [String] else { + return defaultReactions + } + return emojis + } + |> distinctUntilChanged + + let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) + |> map { animatedEmoji -> [String: [StickerPackItem]] in + var animatedEmojiStickers: [String: [StickerPackItem]] = [:] + switch animatedEmoji { + case let .result(_, items, _): + for item in items { + if let emoji = item.getStringRepresentationsOfIndexKeys().first { + animatedEmojiStickers[emoji.basicEmoji.0] = [item] + let strippedEmoji = emoji.basicEmoji.0.strippedEmoji + if animatedEmojiStickers[strippedEmoji] == nil { + animatedEmojiStickers[strippedEmoji] = [item] + } + } + } + default: + break + } + return animatedEmojiStickers + } + + // We are intentionally not subscribing to the recent gif updates here + let gifItems: Signal = context.engine.data.get(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) |> map { savedGifs -> GifPagerContentComponent in var items: [GifPagerContentComponent.Item] = [] for gifItem in savedGifs { @@ -430,6 +465,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return GifPagerContentComponent( context: context, inputInteraction: gifInputInteraction, + subject: .recent, items: items ) } @@ -437,13 +473,23 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return combineLatest(queue: .mainQueue(), emojiItems, stickerItems, - gifItems + gifItems, + reactions, + animatedEmojiStickers ) - |> map { emoji, stickers, gifs -> InputData in + |> map { emoji, stickers, gifs, reactions, animatedEmojiStickers -> InputData in + var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] = [] + for reaction in reactions { + if let file = animatedEmojiStickers[reaction]?.first?.file { + availableGifSearchEmojies.append(EntityKeyboardComponent.GifSearchEmoji(emoji: reaction, file: file, title: reaction)) + } + } + return InputData( emoji: emoji, stickers: stickers, - gifs: gifs + gifs: gifs, + availableGifSearchEmojies: availableGifSearchEmojies ) } } @@ -470,6 +516,13 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)? + private var gifMode: GifPagerContentComponent.Subject = .recent { + didSet { + self.gifModeSubject.set(self.gifMode) + } + } + private let gifModeSubject: ValuePromise + init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction) { self.context = context self.currentInputData = currentInputData @@ -479,17 +532,25 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { self.entityKeyboardView = ComponentHostView() + self.gifModeSubject = ValuePromise(self.gifMode, ignoreRepeated: true) + super.init() self.view.addSubview(self.entityKeyboardView) self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer() - self.inputDataDisposable = (updatedInputData - |> deliverOnMainQueue).start(next: { [weak self] inputData in + self.inputDataDisposable = (combineLatest(queue: .mainQueue(), + updatedInputData, + self.updatedGifs() + ) + |> deliverOnMainQueue).start(next: { [weak self] inputData, gifs in guard let strongSelf = self else { return } + var inputData = inputData + inputData.gifs = gifs + strongSelf.currentInputData = inputData strongSelf.performLayout() }) @@ -531,6 +592,92 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { self.inputDataDisposable?.dispose() } + private func updatedGifs() -> Signal { + let gifInputInteraction = GifPagerContentComponent.InputInteraction( + performItemAction: { [weak controllerInteraction] item, view, rect in + guard let controllerInteraction = controllerInteraction else { + return + } + let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false) + } + ) + + let context = self.context + let trendingGifs = self.trendingGifsPromise.get() + let updatedGifs = self.gifModeSubject.get() + |> mapToSignal { subject -> Signal in + switch subject { + case .recent: + let gifItems: Signal = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) + |> map { savedGifs -> GifPagerContentComponent in + var items: [GifPagerContentComponent.Item] = [] + for gifItem in savedGifs { + items.append(GifPagerContentComponent.Item( + file: gifItem.contents.get(RecentMediaItem.self)!.media + )) + } + return GifPagerContentComponent( + context: context, + inputInteraction: gifInputInteraction, + subject: subject, + items: items + ) + } + return gifItems + case .trending: + return trendingGifs + |> map { trendingGifs -> GifPagerContentComponent in + var items: [GifPagerContentComponent.Item] = [] + + if let trendingGifs = trendingGifs { + for file in trendingGifs.files { + items.append(GifPagerContentComponent.Item( + file: file.file.media + )) + } + } + + return GifPagerContentComponent( + context: context, + inputInteraction: gifInputInteraction, + subject: subject, + items: items + ) + } + case let .emojiSearch(query): + return paneGifSearchForQuery(context: context, query: query, offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil) + |> map { result -> GifPagerContentComponent in + var items: [GifPagerContentComponent.Item] = [] + + /*let canLoadMore: Bool + if let result = result { + canLoadMore = !result.isComplete + } else { + canLoadMore = true + }*/ + + if let result = result { + for file in result.files { + items.append(GifPagerContentComponent.Item( + file: file.file.media + )) + } + } + + return GifPagerContentComponent( + context: context, + inputInteraction: gifInputInteraction, + subject: subject, + items: items + ) + } + } + } + + return .single(self.currentInputData.gifs) + |> then(updatedGifs) + } + func markInputCollapsed() { self.isMarkInputCollapsed = true } @@ -574,6 +721,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { emojiContent: self.currentInputData.emoji, stickerContent: self.currentInputData.stickers, gifContent: self.currentInputData.gifs, + availableGifSearchEmojies: self.currentInputData.availableGifSearchEmojies, defaultToEmojiTab: self.defaultToEmojiTab, externalTopPanelContainer: self.externalTopPanelContainerImpl, topPanelExtensionUpdated: { [weak self] topPanelExtension, transition in @@ -603,6 +751,12 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return .text } }, + switchToGifSubject: { [weak self] subject in + guard let strongSelf = self else { + return + } + strongSelf.gifModeSubject.set(subject) + }, makeSearchContainerNode: { content in let mappedMode: ChatMediaInputSearchMode switch content { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index 54f4784e65..f56092af6e 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -5,7 +5,7 @@ import TelegramCore import AccountContext import ChatPresentationInterfaceState -func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: AccessoryPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? { +func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: AccessoryPanelNode?, chatControllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? { if let _ = chatPresentationInterfaceState.interfaceState.selectionState { return nil } @@ -70,7 +70,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS 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) + 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) panelNode.interfaceInteraction = interfaceInteraction return panelNode } diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 4bc9f720fa..cf292275b6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1017,7 +1017,17 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] { if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId { } else { - replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, item.message, CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude)) + replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments( + presentationData: item.presentationData, + strings: item.presentationData.strings, + context: item.context, + type: .standalone, + message: replyMessage, + parentMessage: item.message, + constrainedSize: CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer + )) } } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty { replyMarkup = attribute diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 36ba7e2138..20e0337914 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -530,13 +530,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode contentNode.visibility = mapVisibility(self.visibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) } - /*switch self.visibility { - case let .visible(_, subRect): - let topEdge = self.bounds.height - self.insets.top - (subRect.origin.y + subRect.height) - self.debugNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topEdge), size: CGSize(width: 100.0, height: 2.0)) - case .none: - break - }*/ + if let replyInfoNode = self.replyInfoNode { + replyInfoNode.visibility = self.visibility != .none + } } } } @@ -1039,7 +1035,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), - replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode), + replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, () -> ChatMessageReplyInfoNode), actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, 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)), @@ -1802,7 +1798,17 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } else { headerSize.height += 2.0 } - let sizeAndApply = replyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .bubble(incoming: incoming), replyMessage, item.message, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude)) + let sizeAndApply = replyInfoLayout(ChatMessageReplyInfoNode.Arguments( + presentationData: item.presentationData, + strings: item.presentationData.strings, + context: item.context, + type: .bubble(incoming: incoming), + message: replyMessage, + parentMessage: item.message, + constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer + )) replyInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() }) replyInfoOriginY = headerSize.height @@ -2489,6 +2495,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if replyInfoNode.supernode == nil { strongSelf.clippingNode.addSubnode(replyInfoNode) animateFrame = false + + replyInfoNode.visibility = strongSelf.visibility != .none } let previousReplyInfoNodeFrame = replyInfoNode.frame replyInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + replyInfoOriginY), size: replyInfoSizeApply.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 6fc9c5eddb..1034a9c0a5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -458,7 +458,17 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] { if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId { } else { - replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, item.message, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) + replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments( + presentationData: item.presentationData, + strings: item.presentationData.strings, + context: item.context, + type: .standalone, + message: replyMessage, + parentMessage: item.message, + constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer + )) } } else if let _ = attribute as? InlineBotMessageAttribute { } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty { diff --git a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift index 23ef6310c4..d61ba7bd2a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift @@ -196,10 +196,10 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { } if messageEntities?.count == 0 { messageEntities = nil - messageText = textString + messageText = textString.string } } else { - messageText = textString + messageText = textString.string } } else if item.messages.count > 1, let peer = item.messages[0].peers[item.messages[0].id.peerId] { var displayAuthor = true diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift index 0f6935d5ba..6eabcd003c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift @@ -12,6 +12,9 @@ import PhotoResources import TelegramStringFormatting import TextFormat import InvisibleInkDustNode +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer enum ChatMessageReplyInfoType { case bubble(incoming: Bool) @@ -19,10 +22,52 @@ enum ChatMessageReplyInfoType { } class ChatMessageReplyInfoNode: ASDisplayNode { + class Arguments { + let presentationData: ChatPresentationData + let strings: PresentationStrings + let context: AccountContext + let type: ChatMessageReplyInfoType + let message: Message + let parentMessage: Message + let constrainedSize: CGSize + let animationCache: AnimationCache? + let animationRenderer: MultiAnimationRenderer? + + init( + presentationData: ChatPresentationData, + strings: PresentationStrings, + context: AccountContext, + type: ChatMessageReplyInfoType, + message: Message, + parentMessage: Message, + constrainedSize: CGSize, + animationCache: AnimationCache?, + animationRenderer: MultiAnimationRenderer? + ) { + self.presentationData = presentationData + self.strings = strings + self.context = context + self.type = type + self.message = message + self.parentMessage = parentMessage + self.constrainedSize = constrainedSize + self.animationCache = animationCache + self.animationRenderer = animationRenderer + } + } + + var visibility: Bool = false { + didSet { + if self.visibility != oldValue { + self.textNode?.visibilityRect = self.visibility ? CGRect.infinite : nil + } + } + } + private let contentNode: ASDisplayNode private let lineNode: ASImageNode private var titleNode: TextNode? - private var textNode: TextNode? + private var textNode: TextNodeWithEntities? private var dustNode: InvisibleInkDustNode? private var imageNode: TransformImageNode? private var previousMediaReference: AnyMediaReference? @@ -45,31 +90,33 @@ class ChatMessageReplyInfoNode: ASDisplayNode { self.contentNode.addSubnode(self.lineNode) } - class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ theme: ChatPresentationData, _ strings: PresentationStrings, _ context: AccountContext, _ type: ChatMessageReplyInfoType, _ message: Message, _ parentMessage: Message, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode) { + class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, () -> ChatMessageReplyInfoNode) { let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode) - let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode) + let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode) let imageNodeLayout = TransformImageNode.asyncLayout(maybeNode?.imageNode) let previousMediaReference = maybeNode?.previousMediaReference - return { presentationData, strings, context, type, message, parentMessage, constrainedSize in - let fontSize = floor(presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) + return { arguments in + //presentationData, strings, context, type, message, parentMessage, constrainedSize + + let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) let titleFont = Font.medium(fontSize) let textFont = Font.regular(fontSize) - let author = message.effectiveAuthor - var titleString = author.flatMap(EnginePeer.init)?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) ?? strings.User_DeletedAccount + let author = arguments.message.effectiveAuthor + var titleString = author.flatMap(EnginePeer.init)?.displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) ?? arguments.strings.User_DeletedAccount - if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) || parentMessage.forwardInfo != nil { + if let forwardInfo = arguments.message.forwardInfo, forwardInfo.flags.contains(.isImported) || arguments.parentMessage.forwardInfo != nil { if let author = forwardInfo.author { - titleString = EnginePeer(author).displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) + titleString = EnginePeer(author).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) } else if let authorSignature = forwardInfo.authorSignature { titleString = authorSignature } } - let (textString, isMedia, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: context.account.peerId) + let (textString, isMedia, isText) = descriptionStringForMessage(contentSettings: arguments.context.currentContentSettings.with { $0 }, message: EngineMessage(arguments.message), strings: arguments.strings, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, dateTimeFormat: arguments.presentationData.dateTimeFormat, accountPeerId: arguments.context.account.peerId) - let placeholderColor: UIColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor + let placeholderColor: UIColor = arguments.message.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 @@ -77,11 +124,11 @@ class ChatMessageReplyInfoNode: ASDisplayNode { var authorNameColor: UIColor? - if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(parentMessage.id.peerId.namespace) && author?.id.namespace == Namespaces.Peer.CloudUser { + if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(arguments.parentMessage.id.peerId.namespace) && author?.id.namespace == Namespaces.Peer.CloudUser { authorNameColor = author.flatMap { chatMessagePeerIdColors[Int(clamping: $0.id.id._internalGetInt64Value() % 7)] } if let rawAuthorNameColor = authorNameColor { var dimColors = false - switch presentationData.theme.theme.name { + switch arguments.presentationData.theme.theme.name { case .builtin(.nightAccent), .builtin(.night): dimColors = true default: @@ -97,21 +144,21 @@ class ChatMessageReplyInfoNode: ASDisplayNode { } } - switch type { + switch arguments.type { case let .bubble(incoming): - titleColor = incoming ? (authorNameColor ?? presentationData.theme.theme.chat.message.incoming.accentTextColor) : presentationData.theme.theme.chat.message.outgoing.accentTextColor - lineImage = incoming ? (authorNameColor.flatMap({ PresentationResourcesChat.chatBubbleVerticalLineImage(color: $0) }) ?? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(presentationData.theme.theme)) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(presentationData.theme.theme) + 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 isMedia { - textColor = incoming ? presentationData.theme.theme.chat.message.incoming.secondaryTextColor : presentationData.theme.theme.chat.message.outgoing.secondaryTextColor + textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor } else { - textColor = incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor + textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.primaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.primaryTextColor } - dustColor = incoming ? presentationData.theme.theme.chat.message.incoming.secondaryTextColor : presentationData.theme.theme.chat.message.outgoing.secondaryTextColor + dustColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor case .standalone: - let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) + let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) titleColor = serviceColor.primaryText - let graphics = PresentationResourcesChat.additionalGraphics(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) + let graphics = PresentationResourcesChat.additionalGraphics(arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners) lineImage = graphics.chatServiceVerticalLineImage textColor = titleColor dustColor = titleColor @@ -120,20 +167,22 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let messageText: NSAttributedString if isText { - let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in + let entities = (arguments.message.textEntitiesAttribute?.entities ?? []).filter { entity in if case .Spoiler = entity.type { return true + } else if case .CustomEmoji = entity.type { + return true } else { return false } } if entities.count > 0 { - messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) + messageText = stringWithAppliedEntities(trimToLineCount(arguments.message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) } else { - messageText = NSAttributedString(string: textString, font: textFont, textColor: textColor) + messageText = NSAttributedString(string: textString.string, font: textFont, textColor: textColor) } } else { - messageText = NSAttributedString(string: textString, font: textFont, textColor: textColor) + messageText = NSAttributedString(string: textString.string, font: textFont, textColor: textColor) } var leftInset: CGFloat = 11.0 @@ -142,16 +191,16 @@ class ChatMessageReplyInfoNode: ASDisplayNode { var updatedMediaReference: AnyMediaReference? var imageDimensions: CGSize? var hasRoundImage = false - if !message.containsSecretMedia { - for media in message.media { + if !arguments.message.containsSecretMedia { + for media in arguments.message.media { if let image = media as? TelegramMediaImage { - updatedMediaReference = .message(message: MessageReference(message), media: image) + updatedMediaReference = .message(message: MessageReference(arguments.message), media: image) if let representation = largestRepresentationForPhoto(image) { imageDimensions = representation.dimensions.cgSize } break } else if let file = media as? TelegramMediaFile, file.isVideo && !file.isVideoSticker { - updatedMediaReference = .message(message: MessageReference(message), media: file) + updatedMediaReference = .message(message: MessageReference(arguments.message), media: file) if let dimensions = file.dimensions { imageDimensions = dimensions.cgSize @@ -168,12 +217,12 @@ class ChatMessageReplyInfoNode: ASDisplayNode { var imageTextInset: CGFloat = 0.0 if let _ = imageDimensions { - imageTextInset += floor(presentationData.fontSize.baseDisplaySize * 32.0 / 17.0) + imageTextInset += floor(arguments.presentationData.fontSize.baseDisplaySize * 32.0 / 17.0) } - let maximumTextWidth = max(0.0, constrainedSize.width - imageTextInset) + let maximumTextWidth = max(0.0, arguments.constrainedSize.width - imageTextInset) - let contrainedTextSize = CGSize(width: maximumTextWidth, height: constrainedSize.height) + let contrainedTextSize = CGSize(width: maximumTextWidth, height: arguments.constrainedSize.height) let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) @@ -206,12 +255,12 @@ class ChatMessageReplyInfoNode: ASDisplayNode { var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if let updatedMediaReference = updatedMediaReference, mediaUpdated && imageDimensions != nil { if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { - updateImageSignal = chatMessagePhotoThumbnail(account: context.account, photoReference: imageReference) + updateImageSignal = chatMessagePhotoThumbnail(account: arguments.context.account, photoReference: imageReference) } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { if fileReference.media.isVideo { - updateImageSignal = chatMessageVideoThumbnail(account: context.account, fileReference: fileReference) + updateImageSignal = chatMessageVideoThumbnail(account: arguments.context.account, fileReference: fileReference) } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { - updateImageSignal = chatWebpageSnippetFile(account: context.account, mediaReference: fileReference.abstract, representation: iconImageRepresentation) + updateImageSignal = chatWebpageSnippetFile(account: arguments.context.account, mediaReference: fileReference.abstract, representation: iconImageRepresentation) } } } @@ -228,11 +277,16 @@ class ChatMessageReplyInfoNode: ASDisplayNode { node.previousMediaReference = updatedMediaReference - node.titleNode?.displaysAsynchronously = !presentationData.isPreview - node.textNode?.displaysAsynchronously = !presentationData.isPreview + node.titleNode?.displaysAsynchronously = !arguments.presentationData.isPreview + node.textNode?.textNode.displaysAsynchronously = !arguments.presentationData.isPreview let titleNode = titleApply() - let textNode = textApply() + var textArguments: TextNodeWithEntities.Arguments? + if let cache = arguments.animationCache, let renderer = arguments.animationRenderer { + textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: placeholderColor) + } + let textNode = textApply(textArguments) + textNode.visibilityRect = node.visibility ? CGRect.infinite : nil if node.titleNode == nil { titleNode.isUserInteractionEnabled = false @@ -241,9 +295,9 @@ class ChatMessageReplyInfoNode: ASDisplayNode { } if node.textNode == nil { - textNode.isUserInteractionEnabled = false + textNode.textNode.isUserInteractionEnabled = false node.textNode = textNode - node.contentNode.addSubnode(textNode) + node.contentNode.addSubnode(textNode.textNode) } if let applyImage = applyImage { @@ -262,12 +316,12 @@ class ChatMessageReplyInfoNode: ASDisplayNode { imageNode.removeFromSupernode() node.imageNode = nil } - node.imageNode?.captureProtected = message.isCopyProtected() + node.imageNode?.captureProtected = arguments.message.isCopyProtected() 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) - textNode.frame = textFrame + textNode.textNode.frame = textFrame if !textLayout.spoilers.isEmpty { let dustNode: InvisibleInkDustNode @@ -277,7 +331,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { dustNode = InvisibleInkDustNode(textNode: nil) dustNode.isUserInteractionEnabled = false node.dustNode = dustNode - node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode) + node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode.textNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) dustNode.update(size: dustNode.frame.size, color: dustColor, textColor: dustColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) @@ -344,16 +398,16 @@ class ChatMessageReplyInfoNode: ASDisplayNode { if let textNode = self.textNode { let offset = CGPoint( - x: localRect.minX + sourceReplyPanel.textNode.frame.minX - textNode.frame.minX, - y: localRect.minY + sourceReplyPanel.textNode.frame.midY - textNode.frame.midY + x: localRect.minX + sourceReplyPanel.textNode.frame.minX - textNode.textNode.frame.minX, + y: localRect.minY + sourceReplyPanel.textNode.frame.midY - textNode.textNode.frame.midY ) - transition.horizontal.animatePositionAdditive(node: textNode, offset: CGPoint(x: offset.x, y: 0.0)) - transition.vertical.animatePositionAdditive(node: textNode, offset: CGPoint(x: 0.0, y: offset.y)) + transition.horizontal.animatePositionAdditive(node: textNode.textNode, offset: CGPoint(x: offset.x, y: 0.0)) + transition.vertical.animatePositionAdditive(node: textNode.textNode, offset: CGPoint(x: 0.0, y: offset.y)) sourceParentNode.addSubnode(sourceReplyPanel.textNode) - textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) sourceReplyPanel.textNode.frame = sourceReplyPanel.textNode.frame .offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y) diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 2f57d3b0a4..941f83c408 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -570,7 +570,17 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] { if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId { } else { - replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, item.message, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) + replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments( + presentationData: item.presentationData, + strings: item.presentationData.strings, + context: item.context, + type: .standalone, + message: replyMessage, + parentMessage: item.message, + constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer + )) } } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty { replyMarkup = attribute diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index b5c56c774e..d6a2bb8278 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -19,6 +19,7 @@ import AnimationCache import LottieAnimationCache import MultiAnimationRenderer import EmojiTextAttachmentView +import TextNodeWithEntities private final class CachedChatMessageText { let text: String @@ -46,27 +47,8 @@ private final class CachedChatMessageText { } } -private final class InlineStickerItem: Hashable { - let emoji: ChatTextInputTextCustomEmojiAttribute - - init(emoji: ChatTextInputTextCustomEmojiAttribute) { - self.emoji = emoji - } - - func hash(into hasher: inout Hasher) { - hasher.combine(emoji.fileId) - } - - static func ==(lhs: InlineStickerItem, rhs: InlineStickerItem) -> Bool { - if lhs.emoji.fileId != rhs.emoji.fileId { - return false - } - return true - } -} - class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { - private let textNode: TextNode + private let textNode: TextNodeWithEntities private var spoilerTextNode: TextNode? private var dustNode: InvisibleInkDustNode? @@ -76,42 +58,27 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private var textSelectionNode: TextSelectionNode? private var textHighlightingNodes: [LinkHighlightingNode] = [] - private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:] private var cachedChatMessageText: CachedChatMessageText? - private var isVisibleForAnimations: Bool { - return self.visibility != .none - } - override var visibility: ListViewItemNodeVisibility { didSet { - if !self.inlineStickerItemLayers.isEmpty { - if oldValue != self.visibility { - for (_, itemLayer) in self.inlineStickerItemLayers { - let isItemVisible: Bool - switch self.visibility { - case .none: - isItemVisible = false - case let .visible(_, subRect): - var subRect = subRect - subRect.origin.x = 0.0 - subRect.size.width = 10000.0 - if itemLayer.frame.intersects(subRect) { - isItemVisible = true - } else { - isItemVisible = false - } - } - itemLayer.isVisibleForAnimations = isItemVisible - } + if oldValue != self.visibility { + switch self.visibility { + case .none: + self.textNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + self.textNode.visibilityRect = subRect } } } } required init() { - self.textNode = TextNode() + self.textNode = TextNodeWithEntities() self.statusNode = ChatMessageDateAndStatusNode() @@ -119,11 +86,11 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { super.init() - self.textNode.isUserInteractionEnabled = false - self.textNode.contentMode = .topLeft - self.textNode.contentsScale = UIScreenScale - self.textNode.displaysAsynchronously = true - self.addSubnode(self.textNode) + self.textNode.textNode.isUserInteractionEnabled = false + self.textNode.textNode.contentMode = .topLeft + self.textNode.textNode.contentsScale = UIScreenScale + self.textNode.textNode.displaysAsynchronously = true + self.addSubnode(self.textNode.textNode) self.addSubnode(self.textAccessibilityOverlayNode) self.textAccessibilityOverlayNode.openUrl = { [weak self] url in @@ -152,7 +119,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { - let textLayout = TextNode.asyncLayout(self.textNode) + let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) let spoilerTextLayout = TextNode.asyncLayout(self.spoilerTextNode) let statusLayout = self.statusNode.asyncLayout() @@ -354,7 +321,6 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil) var updatedAttributes: [NSAttributedString.Key: Any] = currentDict updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor - updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId)) updatedAttributes[ChatTextInputAttributes.customEmoji] = ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId) let insertString = NSAttributedString(string: updatedString.attributedSubstring(from: range).string, attributes: updatedAttributes) @@ -445,29 +411,29 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.cachedChatMessageText = updatedCachedChatMessageText } - let cachedLayout = strongSelf.textNode.cachedLayout + let cachedLayout = strongSelf.textNode.textNode.cachedLayout if case .System = animation { if let cachedLayout = cachedLayout { if !cachedLayout.areLinesEqual(to: textLayout) { - if let textContents = strongSelf.textNode.contents { + if let textContents = strongSelf.textNode.textNode.contents { let fadeNode = ASDisplayNode() fadeNode.displaysAsynchronously = false fadeNode.contents = textContents - fadeNode.frame = strongSelf.textNode.frame + fadeNode.frame = strongSelf.textNode.textNode.frame fadeNode.isLayerBacked = true strongSelf.addSubnode(fadeNode) fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in fadeNode?.removeFromSupernode() }) - strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } } } } - let _ = textApply() - animation.animator.updateFrame(layer: strongSelf.textNode.layer, frame: textFrame, completion: nil) + let _ = textApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: messageTheme.mediaPlaceholderColor)) + animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil) if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { let spoilerTextNode = spoilerTextApply() @@ -514,8 +480,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.textAccessibilityOverlayNode.frame = textFrame strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout - - strongSelf.updateInlineStickers(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, textLayout: textLayout, placeholderColor: messageTheme.mediaPlaceholderColor) + if let statusSizeAndApply = statusSizeAndApply { animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0), completion: nil) @@ -546,73 +511,29 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor) { - var nextIndexById: [Int64: Int] = [:] - var validIds: [InlineStickerItemLayer.Key] = [] - - if let textLayout = textLayout { - for item in textLayout.embeddedItems { - if let stickerItem = item.value as? InlineStickerItem { - let index: Int - if let currentNext = nextIndexById[stickerItem.emoji.fileId] { - index = currentNext - } else { - index = 0 - } - nextIndexById[stickerItem.emoji.fileId] = index + 1 - let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index) - validIds.append(id) - - let itemLayer: InlineStickerItemLayer - if let current = self.inlineStickerItemLayers[id] { - itemLayer = current - } else { - itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, emoji: stickerItem.emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor) - self.inlineStickerItemLayers[id] = itemLayer - self.textNode.layer.addSublayer(itemLayer) - itemLayer.isVisibleForAnimations = self.isVisibleForAnimations - } - - itemLayer.frame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left, dy: textLayout.insets.top + 0.0).center, size: CGSize()).insetBy(dx: -12.0, dy: -12.0) - } - } - } - - var removeKeys: [InlineStickerItemLayer.Key] = [] - for (key, itemLayer) in self.inlineStickerItemLayers { - if !validIds.contains(key) { - removeKeys.append(key) - itemLayer.removeFromSuperlayer() - } - } - for key in removeKeys { - self.inlineStickerItemLayers.removeValue(forKey: key) - } - } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + 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) { - self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + 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) { - self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + 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 { - let textNodeFrame = self.textNode.frame - if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + 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) { return .none } else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true - if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } return .url(url: url, concealed: concealed) @@ -669,8 +590,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var rects: [CGRect]? var spoilerRects: [CGRect]? if let point = point { - let textNodeFrame = self.textNode.frame - if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + 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)) { let possibleNames: [String] = [ TelegramTextAttributes.URL, TelegramTextAttributes.PeerMention, @@ -682,12 +603,12 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { - rects = self.textNode.attributeRects(name: name, at: index) + rects = self.textNode.textNode.attributeRects(name: name, at: index) break } } if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)] { - spoilerRects = self.textNode.attributeRects(name: TelegramTextAttributes.Spoiler, at: index) + spoilerRects = self.textNode.textNode.attributeRects(name: TelegramTextAttributes.Spoiler, at: index) } } } @@ -701,9 +622,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor) self.linkHighlightingNode = linkHighlightingNode - self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) + self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode) } - linkHighlightingNode.frame = self.textNode.frame + linkHighlightingNode.frame = self.textNode.textNode.frame linkHighlightingNode.updateRects(rects) } else if let linkHighlightingNode = self.linkHighlightingNode { self.linkHighlightingNode = nil @@ -716,16 +637,16 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { if let item = self.item { - let textNodeFrame = self.textNode.frame - if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + 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.attributeRects(name: TelegramTextAttributes.URL, at: index), !rects.isEmpty { + 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.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + 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)) @@ -742,7 +663,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } let rectsSet: [[CGRect]] if let text = text, let messages = messages, !text.isEmpty, messages.contains(item.message.index) { - rectsSet = self.textNode.textRangesRects(text: text) + rectsSet = self.textNode.textNode.textRangesRects(text: text) } else { rectsSet = [] } @@ -754,9 +675,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.textHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.textHighlightColor) self.textHighlightingNodes.append(textHighlightNode) - self.insertSubnode(textHighlightNode, belowSubnode: self.textNode) + self.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode) } - textHighlightNode.frame = self.textNode.frame + textHighlightNode.frame = self.textNode.textNode.frame textHighlightNode.updateRects(rects) } for i in (rectsSet.count ..< self.textHighlightingNodes.count).reversed() { @@ -791,7 +712,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { knobColor = item.presentationData.theme.theme.chat.message.outgoing.textSelectionKnobColor } - let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor), strings: item.presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in + let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor), strings: item.presentationData.strings, textNode: self.textNode.textNode, updateIsActive: { [weak self] value in self?.updateIsTextSelectionActive?(value) }, present: { [weak self] c, a in self?.item?.controllerInteraction.presentGlobalOverlayController(c, a) @@ -802,7 +723,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { item.controllerInteraction.performTextSelectionAction(item.message.stableId, text, action) }) textSelectionNode.updateRange = { [weak self] selectionRange in - if let strongSelf = self, let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange { + if let strongSelf = self, let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange { for (spoilerRange, _) in textLayout.spoilers { if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 { dustNode.update(revealed: true) @@ -813,9 +734,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } self.textSelectionNode = textSelectionNode self.addSubnode(textSelectionNode) - self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode) - textSelectionNode.frame = self.textNode.frame - textSelectionNode.highlightAreaNode.frame = self.textNode.frame + self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode.textNode) + textSelectionNode.frame = self.textNode.textNode.frame + textSelectionNode.highlightAreaNode.frame = self.textNode.textNode.frame } } else { if let textSelectionNode = self.textSelectionNode { @@ -851,14 +772,14 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { sourceView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak sourceView] _ in sourceView?.removeFromSuperview() }) - self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.08) + self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.08) let offset = CGPoint( - x: sourceView.frame.minX - (self.textNode.frame.minX - 0.0), - y: sourceView.frame.minY - (self.textNode.frame.minY - 3.0) - scrollOffset + x: sourceView.frame.minX - (self.textNode.textNode.frame.minX - 0.0), + y: sourceView.frame.minY - (self.textNode.textNode.frame.minY - 3.0) - scrollOffset ) - transition.vertical.animatePositionAdditive(node: self.textNode, offset: offset) + transition.vertical.animatePositionAdditive(node: self.textNode.textNode, offset: offset) transition.updatePosition(layer: sourceView.layer, position: CGPoint(x: sourceView.layer.position.x - offset.x, y: sourceView.layer.position.y - offset.y)) self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index f3843008d4..10382a35e1 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -650,10 +650,10 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { if entities.count > 0 { messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) } else { - messageText = NSAttributedString(string: foldLineBreaks(textString), font: textFont, textColor: textColor) + messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: textColor) } } else { - messageText = NSAttributedString(string: foldLineBreaks(textString), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor) + messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor) } let textConstrainedSize = CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude) diff --git a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift index ff30790408..70af8117f2 100644 --- a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift @@ -170,7 +170,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { if let currentEditMediaReference = self.currentEditMediaReference { effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) } - (text, _, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(effectiveMessage), strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId) + let (attributedText, _, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(effectiveMessage), strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId) + text = attributedText.string } var updatedMediaReference: AnyMediaReference? diff --git a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift index b1fc27e49a..54b0552aee 100644 --- a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift @@ -14,6 +14,9 @@ import TelegramStringFormatting import InvisibleInkDustNode import TextFormat import ChatPresentationInterfaceState +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer final class ReplyAccessoryPanelNode: AccessoryPanelNode { private let messageDisposable = MetaDisposable() @@ -25,7 +28,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { let lineNode: ASImageNode let iconNode: ASImageNode let titleNode: ImmediateTextNode - let textNode: ImmediateTextNode + let textNode: ImmediateTextNodeWithEntities var dustNode: InvisibleInkDustNode? let imageNode: TransformImageNode @@ -36,7 +39,7 @@ 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) { + init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { self.messageId = messageId self.theme = theme @@ -63,11 +66,20 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.titleNode.displaysAsynchronously = false self.titleNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) - self.textNode = ImmediateTextNode() + self.textNode = ImmediateTextNodeWithEntities() self.textNode.maximumNumberOfLines = 1 self.textNode.displaysAsynchronously = false self.textNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) + if let animationCache = animationCache, let animationRenderer = animationRenderer { + self.textNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor + ) + } + self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] self.imageNode.isHidden = true @@ -119,7 +131,9 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { default: isMedia = true } - (text, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) + let (attributedText, _, isTextValue) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) + text = attributedText.string + isText = isTextValue } else { isMedia = false } @@ -128,9 +142,10 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { let messageText: NSAttributedString if isText, let message = message { let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in - if case .Spoiler = entity.type { + switch entity.type { + case .Spoiler, .CustomEmoji: return true - } else { + default: return false } } diff --git a/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift index c4a682e23d..f588bb8561 100644 --- a/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift @@ -135,14 +135,14 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { } else if content.type == "telegram_theme" { text = strings.Message_Theme } else { - text = stringForMediaKind(mediaKind, strings: self.strings).0 + text = stringForMediaKind(mediaKind, strings: self.strings).0.string } } else if content.type == "telegram_theme" { text = strings.Message_Theme } else if content.type == "video" { - text = stringForMediaKind(.video, strings: self.strings).0 + text = stringForMediaKind(.video, strings: self.strings).0.string } else if let _ = content.image { - text = stringForMediaKind(.image, strings: self.strings).0 + text = stringForMediaKind(.image, strings: self.strings).0.string } }