diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 93fbc675e7..52ad3e640f 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -16369,3 +16369,7 @@ Error: %8$@"; "WebBrowser.Exceptions.DontOpenInApp" = "DON'T OPEN IN-APP"; "WebBrowser.Exceptions.InAppInfo" = "These sites will still be opened in-app."; "WebBrowser.Exceptions.DeleteAll" = "Delete All Exceptions"; + +"RichTextPreview.Formula" = "[formula]"; +"RichTextPreview.Table" = "[table]"; +"RichTextPreview.Music" = "Music"; diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index e385d285a3..8872c90184 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -104,7 +104,7 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: messageText = "" for message in messages { if let richText = message.richText { - messageText = richText.instantPage.previewText() + messageText = richText.instantPage.previewText(strings: strings) messageEntities = [] } else if !message.text.isEmpty { messageText = message.text diff --git a/submodules/TelegramStringFormatting/Sources/InstantPagePreviewText.swift b/submodules/TelegramStringFormatting/Sources/InstantPagePreviewText.swift index 47cf199bf6..0f39bb3697 100644 --- a/submodules/TelegramStringFormatting/Sources/InstantPagePreviewText.swift +++ b/submodules/TelegramStringFormatting/Sources/InstantPagePreviewText.swift @@ -1,64 +1,64 @@ import Foundation +import Postbox import TelegramCore +import TelegramPresentationData extension RichText { - public func previewText() -> String { + public func previewText(strings: PresentationStrings) -> String { switch self { case .empty: return "" case let .plain(value): return value case let .bold(value): - return value.previewText() + return value.previewText(strings: strings) case let .italic(value): - return value.previewText() + return value.previewText(strings: strings) case let .underline(value): - return value.previewText() + return value.previewText(strings: strings) case let .strikethrough(value): - return value.previewText() + return value.previewText(strings: strings) case let .fixed(value): - return value.previewText() + return value.previewText(strings: strings) case let .url(value, _, _): - return value.previewText() + return value.previewText(strings: strings) case let .email(value, _): - return value.previewText() + return value.previewText(strings: strings) case let .concat(values): var result = "" for value in values { - result.append(value.previewText()) + result.append(value.previewText(strings: strings)) } return result case let .`subscript`(value): - return value.previewText() + return value.previewText(strings: strings) case let .superscript(value): - return value.previewText() + return value.previewText(strings: strings) case let .marked(value): - return value.previewText() + return value.previewText(strings: strings) case let .phone(value, _): - return value.previewText() + return value.previewText(strings: strings) case .image: - //TODO:localize - return "Photo" + return strings.Message_Photo case let .anchor(value, _): - return value.previewText() + return value.previewText(strings: strings) case .formula: - //TODO:localize - return "Fx" + return strings.RichTextPreview_Formula case let .textCustomEmoji(_, alt): return alt case let .textAutoEmail(value), let .textAutoPhone(value), let .textAutoUrl(value), let .textBankCard(value), let .textBotCommand(value), let .textCashtag(value), let .textHashtag(value), let .textMention(value), let .textMentionName(value, _), let .textSpoiler(value), let .textDate(value, _, _): - return value.previewText() + return value.previewText(strings: strings) } } } extension InstantPageListItem { - public func previewText() -> String { + public func previewText(strings: PresentationStrings, media: [MediaId: Media]) -> String { switch self { case .unknown: return "" case let .text(text, num, checked): - let body = text.previewText() + let body = text.previewText(strings: strings) if let checked { return "\(checked ? "☑︎" : "☐") \(body)" } else if let num, !num.isEmpty { @@ -72,7 +72,7 @@ extension InstantPageListItem { if !blocksText.isEmpty { blocksText.append("\n") } - blocksText.append(block.previewText()) + blocksText.append(block.previewText(strings: strings, media: media)) } if let checked { return "\(checked ? "☑︎" : "☐") \(blocksText)" @@ -86,30 +86,30 @@ extension InstantPageListItem { } extension InstantPageBlock { - public func previewText() -> String { + public func previewText(strings: PresentationStrings, media: [MediaId: Media]) -> String { switch self { case .unsupported: return "" case let .title(text): - return text.previewText() + return text.previewText(strings: strings) case let .subtitle(text): - return text.previewText() + return text.previewText(strings: strings) case let .authorDate(author, _): - return author.previewText() + return author.previewText(strings: strings) case let .header(text): - return text.previewText() + return text.previewText(strings: strings) case let .subheader(text): - return text.previewText() + return text.previewText(strings: strings) case let .heading(text, _): - return text.previewText() + return text.previewText(strings: strings) case .formula: - return "Fx" + return strings.RichTextPreview_Formula case let .paragraph(text): - return text.previewText() + return text.previewText(strings: strings) case let .preformatted(text, _): - return text.previewText() + return text.previewText(strings: strings) case let .footer(text): - return text.previewText() + return text.previewText(strings: strings) case .divider: return "\n" case .anchor: @@ -120,23 +120,24 @@ extension InstantPageBlock { if !result.isEmpty { result.append("\n") } - result.append(item.previewText()) + result.append(item.previewText(strings: strings, media: media)) } return result case let .blockQuote(blocks, caption): - let body = blocks.map { $0.previewText() }.joined(separator: " ") - return body + caption.previewText() + let body = blocks.map { $0.previewText(strings: strings, media: media) }.joined(separator: " ") + return body + caption.previewText(strings: strings) case let .pullQuote(text, caption): - return text.previewText() + caption.previewText() + return text.previewText(strings: strings) + caption.previewText(strings: strings) case .image(_, _, _, _): - //TODO:localize - return "Photo" + return strings.Message_Photo case .video(_, _, _, _): - //TODO:localize - return "Video" - case .audio: - //TODO:localize - return "Audio" + return strings.Message_Video + case let .audio(id, _): + if let file = media[id] as? TelegramMediaFile, file.isVoice { + return strings.Message_Audio + } else { + return strings.RichTextPreview_Music + } case .cover: return "" case .webEmbed: @@ -154,28 +155,26 @@ extension InstantPageBlock { case .thinking: return "" case .table: - //TODO:localize - return "Table" + return strings.RichTextPreview_Table case .details: return "" case .relatedArticles: return "" case .map: - //TODO:localize - return "Map" + return strings.Message_Location } } } extension InstantPage { - public func previewText() -> String { + public func previewText(strings: PresentationStrings) -> String { let maxLength: Int = 200 var result = "" for block in self.blocks { if !result.isEmpty { result.append("\n") } - result.append(block.previewText()) + result.append(block.previewText(strings: strings, media: self.media)) if result.count > maxLength { break } diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index d93dfea46e..87925bbfa0 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -314,7 +314,7 @@ public func messageContentKind(contentSettings: ContentSettings, message: Engine } for attribute in message.attributes { if let attribute = attribute as? RichTextMessageAttribute { - return .text(NSAttributedString(string: attribute.instantPage.previewText())) + return .text(NSAttributedString(string: attribute.instantPage.previewText(strings: strings))) } } return .text(messageTextWithAttributes(message: message)) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index f527da9128..a1996db401 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -88,8 +88,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private let containerNode: ContainerNode private let textNode: InteractiveTextNodeWithEntities - private var streamingStatusTextNode: InteractiveTextNodeWithEntities? - private var streamingStatusShimmerView: ShimmeringMaskView? private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode public var statusNode: ChatMessageDateAndStatusNode? @@ -218,7 +216,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let previousItem = self.item let textLayout = InteractiveTextNodeWithEntities.asyncLayout(self.textNode) - let streamingStatusTextLayout = InteractiveTextNodeWithEntities.asyncLayout(self.streamingStatusTextNode) let statusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.statusNode) let currentCachedChatMessageText = self.cachedChatMessageText @@ -697,24 +694,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { computeCharacterRects: true )) - var streamingTextLayoutAndApply: (layout: InteractiveTextNodeLayout, apply: (InteractiveTextNodeWithEntities.Arguments) -> InteractiveTextNodeWithEntities)? - if !"".isEmpty && (hasDraft || hadDraft) { - //TODO:localize - streamingTextLayoutAndApply = streamingStatusTextLayout(InteractiveTextNodeLayoutArguments( - attributedString: NSAttributedString(string: "Thinking...", font: textFont, textColor: messageTheme.fileDescriptionColor), - backgroundColor: nil, - maximumNumberOfLines: 1, - truncationType: .end, - constrainedSize: textConstrainedSize, - alignment: .natural, - cutout: nil, - insets: textInsets, - lineColor: messageTheme.accentControlColor, - customTruncationToken: customTruncationToken, - computeCharacterRects: true - )) - } - var maxGlyphCount = currentMaxGlyphCount if maxGlyphCount == nil && (hasDraft || hadDraft) { maxGlyphCount = previousGlyphCount @@ -775,16 +754,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size) - - let streamingTextSpacing: CGFloat = 1.0 - - var streamingTextFrame: CGRect? - if let streamingTextLayoutAndApply { - let streamingTextFrameValue = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.left - textInsets.left, y: topInset - textInsets.top), size: streamingTextLayoutAndApply.layout.size) - streamingTextFrame = streamingTextFrameValue - textFrame.origin.y += streamingTextFrameValue.height + streamingTextSpacing - textInsets.top - textInsets.bottom - } - + var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom)) textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset) @@ -798,9 +768,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset) var suggestedBoundingWidth: CGFloat = textFrameWithoutInsets.width - if let streamingTextFrame { - suggestedBoundingWidth = max(suggestedBoundingWidth, streamingTextFrame.width - textInsets.left - textInsets.right) - } if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue, !hasDraft { suggestedBoundingWidth = max(suggestedBoundingWidth, statusSuggestedWidthAndContinue.0) } @@ -816,12 +783,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if let statusSizeAndApply = statusSizeAndApply, !hasDraft { boundingSize.height += statusSizeAndApply.0.height } - - if let streamingTextFrame { - boundingSize.width = max(boundingSize.width, streamingTextFrame.width - textInsets.left - textInsets.right) - boundingSize.height += streamingTextFrame.height - textInsets.top - textInsets.bottom + streamingTextSpacing - } - + boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right boundingSize.height += topInset + bottomInset @@ -927,74 +889,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { animation.animator.updatePosition(layer: strongSelf.textNode.textNode.layer, position: realTextFrame.center, completion: nil) animation.animator.updateBounds(layer: strongSelf.textNode.textNode.layer, bounds: CGRect(origin: CGPoint(), size: realTextFrame.size), completion: nil) - if let streamingTextFrame, let streamingTextLayoutAndApply { - var animation = animation - if strongSelf.streamingStatusTextNode == nil { - animation = .None - } - let streamingStatusTextNode = streamingTextLayoutAndApply.apply(InteractiveTextNodeWithEntities.Arguments( - context: item.context, - cache: item.controllerInteraction.presentationContext.animationCache, - renderer: item.controllerInteraction.presentationContext.animationRenderer, - placeholderColor: messageTheme.mediaPlaceholderColor, - attemptSynchronous: synchronousLoads, - textColor: messageTheme.primaryTextColor, - spoilerEffectColor: messageTheme.secondaryTextColor, - applyArguments: InteractiveTextNode.ApplyArguments( - animation: animation, - spoilerTextColor: messageTheme.primaryTextColor, - spoilerEffectColor: messageTheme.secondaryTextColor, - areContentAnimationsEnabled: item.context.sharedContext.energyUsageSettings.loopEmoji, - spoilerExpandRect: nil, - crossfadeContents: { [weak strongSelf] sourceView in - guard let strongSelf, let streamingStatusTextNode = strongSelf.streamingStatusTextNode else { - return - } - if let shimmerView = strongSelf.streamingStatusShimmerView { - sourceView.frame = CGRect(origin: streamingStatusTextNode.textNode.frame.origin, size: sourceView.bounds.size) - shimmerView.addSubview(sourceView) - - sourceView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak sourceView] _ in - sourceView?.removeFromSuperview() - }) - streamingStatusTextNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) - } - } - ) - )) - - let streamingStatusShimmerView: ShimmeringMaskView - if let current = strongSelf.streamingStatusShimmerView { - streamingStatusShimmerView = current - } else { - streamingStatusShimmerView = ShimmeringMaskView(peakAlpha: 0.3, duration: 1.0) - strongSelf.streamingStatusShimmerView = streamingStatusShimmerView - strongSelf.containerNode.view.addSubview(streamingStatusShimmerView) - } - - if streamingStatusTextNode !== strongSelf.streamingStatusTextNode { - strongSelf.streamingStatusTextNode?.textNode.view.removeFromSuperview() - strongSelf.streamingStatusTextNode = streamingStatusTextNode - streamingStatusShimmerView.contentView.addSubview(streamingStatusTextNode.textNode.view) - } - animation.animator.updatePosition(layer: streamingStatusShimmerView.layer, position: streamingTextFrame.center, completion: nil) - animation.animator.updateBounds(layer: streamingStatusShimmerView.layer, bounds: CGRect(origin: CGPoint(), size: streamingTextFrame.size), completion: nil) - animation.animator.updatePosition(layer: streamingStatusTextNode.textNode.layer, position: CGPoint(x: streamingTextFrame.size.width * 0.5, y: streamingTextFrame.size.height * 0.5), completion: nil) - animation.animator.updateBounds(layer: streamingStatusTextNode.textNode.layer, bounds: CGRect(origin: CGPoint(), size: streamingTextFrame.size), completion: nil) - streamingStatusShimmerView.update( - size: streamingTextFrame.size, - containerWidth: streamingTextFrame.size.width, - offsetX: 0.0, - gradientWidth: 200.0, - transition: .immediate - ) - } else if let streamingStatusShimmerView = strongSelf.streamingStatusShimmerView { - strongSelf.streamingStatusTextNode = nil - strongSelf.streamingStatusShimmerView = nil - animation.animator.updateAlpha(layer: streamingStatusShimmerView.layer, alpha: 0.0, completion: { [weak streamingStatusShimmerView] _ in - streamingStatusShimmerView?.removeFromSuperview() - }) - } switch strongSelf.visibility { case .none: