mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Render markdown rich-text preview in send-message context screen
When the user long-presses Send in the main chat and the composed text passes the same rich-markdown gate used at send time (richMarkdownAttributeIfNeeded), the send-options preview bubble renders the message as rich InstantPage content, crossfading from the raw text as the menu animates in. Plain text and custom chat contents / edit previews are unchanged. ChatSendMessageActionUI cannot depend on InstantPageUI (it closes a dependency cycle via LocationUI -> AttachmentUI -> ... -> ChatTextInputActionButtonsNode), so the rich view is built in TelegramUI (ChatSendMessageRichTextPreview, wrapping InstantPageV2View + the outgoing message theme) and injected into the screen via a new ChatSendMessageContextScreenRichTextPreview protocol, mirroring the existing media-preview pattern. The main-chat send call site classifies the compose text and injects the preview; MessageItemView lays it out within the send-button-bounded bubble envelope and crossfades between the raw text and the rich layout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
471d11df16
commit
1d8931b334
5 changed files with 246 additions and 13 deletions
|
|
@ -90,7 +90,8 @@ public func makeChatSendMessageActionSheetController(
|
||||||
openPremiumPaywall: @escaping (ViewController) -> Void,
|
openPremiumPaywall: @escaping (ViewController) -> Void,
|
||||||
reactionItems: [ReactionItem]? = nil,
|
reactionItems: [ReactionItem]? = nil,
|
||||||
availableMessageEffects: AvailableMessageEffects? = nil,
|
availableMessageEffects: AvailableMessageEffects? = nil,
|
||||||
isPremium: Bool = false
|
isPremium: Bool = false,
|
||||||
|
richTextPreview: ChatSendMessageContextScreenRichTextPreview? = nil
|
||||||
) -> ChatSendMessageActionSheetController {
|
) -> ChatSendMessageActionSheetController {
|
||||||
return ChatSendMessageContextScreen(
|
return ChatSendMessageContextScreen(
|
||||||
initialData: initialData,
|
initialData: initialData,
|
||||||
|
|
@ -111,6 +112,7 @@ public func makeChatSendMessageActionSheetController(
|
||||||
openPremiumPaywall: openPremiumPaywall,
|
openPremiumPaywall: openPremiumPaywall,
|
||||||
reactionItems: reactionItems,
|
reactionItems: reactionItems,
|
||||||
availableMessageEffects: availableMessageEffects,
|
availableMessageEffects: availableMessageEffects,
|
||||||
isPremium: isPremium
|
isPremium: isPremium,
|
||||||
|
richTextPreview: richTextPreview
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,13 @@ public protocol ChatSendMessageContextScreenMediaPreview: AnyObject {
|
||||||
func update(containerSize: CGSize, transition: ComponentTransition) -> CGSize
|
func update(containerSize: CGSize, transition: ComponentTransition) -> CGSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public protocol ChatSendMessageContextScreenRichTextPreview: AnyObject {
|
||||||
|
var view: UIView { get }
|
||||||
|
// Lays out the rich content for the given bubble width and theme, returning its content
|
||||||
|
// size. Called every layout pass; the implementation memoizes internally.
|
||||||
|
func update(boundingWidth: CGFloat, presentationData: PresentationData, transition: ComponentTransition) -> CGSize
|
||||||
|
}
|
||||||
|
|
||||||
final class ChatSendMessageContextScreenComponent: Component {
|
final class ChatSendMessageContextScreenComponent: Component {
|
||||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
|
|
||||||
|
|
@ -71,6 +78,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||||
let reactionItems: [ReactionItem]?
|
let reactionItems: [ReactionItem]?
|
||||||
let availableMessageEffects: AvailableMessageEffects?
|
let availableMessageEffects: AvailableMessageEffects?
|
||||||
let isPremium: Bool
|
let isPremium: Bool
|
||||||
|
let richTextPreview: ChatSendMessageContextScreenRichTextPreview?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
initialData: ChatSendMessageContextScreen.InitialData,
|
initialData: ChatSendMessageContextScreen.InitialData,
|
||||||
|
|
@ -91,7 +99,8 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||||
openPremiumPaywall: @escaping (ViewController) -> Void,
|
openPremiumPaywall: @escaping (ViewController) -> Void,
|
||||||
reactionItems: [ReactionItem]?,
|
reactionItems: [ReactionItem]?,
|
||||||
availableMessageEffects: AvailableMessageEffects?,
|
availableMessageEffects: AvailableMessageEffects?,
|
||||||
isPremium: Bool
|
isPremium: Bool,
|
||||||
|
richTextPreview: ChatSendMessageContextScreenRichTextPreview? = nil
|
||||||
) {
|
) {
|
||||||
self.initialData = initialData
|
self.initialData = initialData
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
@ -112,6 +121,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||||
self.reactionItems = reactionItems
|
self.reactionItems = reactionItems
|
||||||
self.availableMessageEffects = availableMessageEffects
|
self.availableMessageEffects = availableMessageEffects
|
||||||
self.isPremium = isPremium
|
self.isPremium = isPremium
|
||||||
|
self.richTextPreview = richTextPreview
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: ChatSendMessageContextScreenComponent, rhs: ChatSendMessageContextScreenComponent) -> Bool {
|
static func ==(lhs: ChatSendMessageContextScreenComponent, rhs: ChatSendMessageContextScreenComponent) -> Bool {
|
||||||
|
|
@ -775,19 +785,35 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||||
if case .editMessage = component.params {
|
if case .editMessage = component.params {
|
||||||
isEditMessage = true
|
isEditMessage = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The bubble's right edge is pinned to the send button (see readyMessageItemFrame).
|
||||||
|
// Constrain the rich layout to the span between that edge and a fixed left margin
|
||||||
|
// (independent of where the source text input sits) so it isn't laid out at the
|
||||||
|
// full container width.
|
||||||
|
let richSendButtonWidth: CGFloat
|
||||||
|
if component.sourceSendButton is ContextExtractedContentContainingView {
|
||||||
|
richSendButtonWidth = sourceSendButtonFrame.width
|
||||||
|
} else {
|
||||||
|
richSendButtonWidth = min(sourceSendButtonFrame.width, 40.0)
|
||||||
|
}
|
||||||
|
let richBubbleRightEdge = sourceSendButtonFrame.maxX - richSendButtonWidth
|
||||||
|
let richBubbleLeftMargin: CGFloat = 16.0
|
||||||
|
let maxRichBubbleWidth = max(1.0, min(messageItemViewContainerSize.width, richBubbleRightEdge - richBubbleLeftMargin))
|
||||||
|
|
||||||
let messageItemSize = messageItemView.update(
|
let messageItemSize = messageItemView.update(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
presentationData: presentationData,
|
presentationData: presentationData,
|
||||||
backgroundNode: wallpaperBackgroundNode,
|
backgroundNode: wallpaperBackgroundNode,
|
||||||
textString: textString,
|
textString: textString,
|
||||||
|
richTextPreview: component.richTextPreview,
|
||||||
|
maxRichBubbleWidth: maxRichBubbleWidth,
|
||||||
sourceTextInputView: component.textInputView as? ChatInputTextView,
|
sourceTextInputView: component.textInputView as? ChatInputTextView,
|
||||||
emojiViewProvider: component.emojiViewProvider,
|
emojiViewProvider: component.emojiViewProvider,
|
||||||
sourceMediaPreview: mediaPreview,
|
sourceMediaPreview: mediaPreview,
|
||||||
mediaCaptionIsAbove: self.mediaCaptionIsAbove,
|
mediaCaptionIsAbove: self.mediaCaptionIsAbove,
|
||||||
textInsets: messageTextInsets,
|
textInsets: messageTextInsets,
|
||||||
explicitBackgroundSize: explicitMessageBackgroundSize,
|
explicitBackgroundSize: explicitMessageBackgroundSize,
|
||||||
maxTextWidth: localSourceTextInputViewFrame.width,
|
maxTextWidth: component.richTextPreview != nil ? maxRichBubbleWidth : localSourceTextInputViewFrame.width,
|
||||||
maxTextHeight: 20000.0,
|
maxTextHeight: 20000.0,
|
||||||
containerSize: messageItemViewContainerSize,
|
containerSize: messageItemViewContainerSize,
|
||||||
effect: self.presentationAnimationState.key == .animatedIn ? self.selectedMessageEffect : nil,
|
effect: self.presentationAnimationState.key == .animatedIn ? self.selectedMessageEffect : nil,
|
||||||
|
|
@ -1469,10 +1495,11 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha
|
||||||
openPremiumPaywall: @escaping (ViewController) -> Void,
|
openPremiumPaywall: @escaping (ViewController) -> Void,
|
||||||
reactionItems: [ReactionItem]?,
|
reactionItems: [ReactionItem]?,
|
||||||
availableMessageEffects: AvailableMessageEffects?,
|
availableMessageEffects: AvailableMessageEffects?,
|
||||||
isPremium: Bool
|
isPremium: Bool,
|
||||||
|
richTextPreview: ChatSendMessageContextScreenRichTextPreview? = nil
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
super.init(
|
super.init(
|
||||||
context: context,
|
context: context,
|
||||||
component: ChatSendMessageContextScreenComponent(
|
component: ChatSendMessageContextScreenComponent(
|
||||||
|
|
@ -1494,7 +1521,8 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha
|
||||||
openPremiumPaywall: openPremiumPaywall,
|
openPremiumPaywall: openPremiumPaywall,
|
||||||
reactionItems: reactionItems,
|
reactionItems: reactionItems,
|
||||||
availableMessageEffects: availableMessageEffects,
|
availableMessageEffects: availableMessageEffects,
|
||||||
isPremium: isPremium
|
isPremium: isPremium,
|
||||||
|
richTextPreview: richTextPreview
|
||||||
),
|
),
|
||||||
navigationBarAppearance: .none,
|
navigationBarAppearance: .none,
|
||||||
statusBarStyle: .none,
|
statusBarStyle: .none,
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@ final class MessageItemView: UIView {
|
||||||
private var customEmojiContainerView: CustomEmojiContainerView?
|
private var customEmojiContainerView: CustomEmojiContainerView?
|
||||||
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
||||||
|
|
||||||
|
private var richTextPreviewView: UIView?
|
||||||
private var mediaPreviewClippingView: UIView?
|
private var mediaPreviewClippingView: UIView?
|
||||||
private var mediaPreview: ChatSendMessageContextScreenMediaPreview?
|
private var mediaPreview: ChatSendMessageContextScreenMediaPreview?
|
||||||
|
|
||||||
|
|
@ -283,6 +284,8 @@ final class MessageItemView: UIView {
|
||||||
presentationData: PresentationData,
|
presentationData: PresentationData,
|
||||||
backgroundNode: WallpaperBackgroundNode?,
|
backgroundNode: WallpaperBackgroundNode?,
|
||||||
textString: NSAttributedString,
|
textString: NSAttributedString,
|
||||||
|
richTextPreview: ChatSendMessageContextScreenRichTextPreview?,
|
||||||
|
maxRichBubbleWidth: CGFloat,
|
||||||
sourceTextInputView: ChatInputTextView?,
|
sourceTextInputView: ChatInputTextView?,
|
||||||
emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?,
|
emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?,
|
||||||
sourceMediaPreview: ChatSendMessageContextScreenMediaPreview?,
|
sourceMediaPreview: ChatSendMessageContextScreenMediaPreview?,
|
||||||
|
|
@ -690,9 +693,38 @@ final class MessageItemView: UIView {
|
||||||
let size = CGSize(width: positionedTextSize.width + textInsets.left + textInsets.right, height: positionedTextSize.height + textInsets.top + textInsets.bottom)
|
let size = CGSize(width: positionedTextSize.width + textInsets.left + textInsets.right, height: positionedTextSize.height + textInsets.top + textInsets.bottom)
|
||||||
|
|
||||||
let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: positionedTextSize)
|
let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: positionedTextSize)
|
||||||
|
|
||||||
let backgroundSize = explicitBackgroundSize ?? size
|
// The outgoing bubble reserves space on the right for its tail (mirrors the
|
||||||
|
// plain-text path's `backgroundSize.width - 1 - 7`); the left keeps a 1pt border.
|
||||||
|
// The injected page lays out within this content width, so it never runs into the
|
||||||
|
// tail and the bubble can't exceed the available container width.
|
||||||
|
let richBubbleLeftInset: CGFloat = 1.0
|
||||||
|
let richBubbleRightInset: CGFloat = 7.0
|
||||||
|
|
||||||
|
var richContentSize: CGSize?
|
||||||
|
if let richTextPreview {
|
||||||
|
let richView = richTextPreview.view
|
||||||
|
if richView.superview !== self {
|
||||||
|
richView.removeFromSuperview()
|
||||||
|
self.addSubview(richView)
|
||||||
|
self.richTextPreviewView = richView
|
||||||
|
}
|
||||||
|
let richBoundingWidth = max(1.0, maxRichBubbleWidth - richBubbleLeftInset - richBubbleRightInset)
|
||||||
|
richContentSize = richTextPreview.update(boundingWidth: richBoundingWidth, presentationData: presentationData, transition: transition)
|
||||||
|
} else if let richTextPreviewView = self.richTextPreviewView {
|
||||||
|
self.richTextPreviewView = nil
|
||||||
|
richTextPreviewView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
let settledContentSize: CGSize
|
||||||
|
if let richContentSize {
|
||||||
|
// Height = 1pt top inset + content + 5pt bottom inset (page sits at y = 1).
|
||||||
|
settledContentSize = CGSize(width: richContentSize.width + richBubbleLeftInset + richBubbleRightInset, height: richContentSize.height + 6.0)
|
||||||
|
} else {
|
||||||
|
settledContentSize = size
|
||||||
|
}
|
||||||
|
let backgroundSize = explicitBackgroundSize ?? settledContentSize
|
||||||
|
|
||||||
let previousSize = self.currentSize
|
let previousSize = self.currentSize
|
||||||
self.currentSize = backgroundSize
|
self.currentSize = backgroundSize
|
||||||
|
|
||||||
|
|
@ -711,7 +743,20 @@ final class MessageItemView: UIView {
|
||||||
|
|
||||||
textNode.view.frame = CGRect(origin: CGPoint(x: textFrame.minX + textPositioningInsets.left - textClippingContainerFrame.minX, y: textFrame.minY + textPositioningInsets.top - textClippingContainerFrame.minY), size: CGSize(width: maxTextWidth, height: textHeight))
|
textNode.view.frame = CGRect(origin: CGPoint(x: textFrame.minX + textPositioningInsets.left - textClippingContainerFrame.minX, y: textFrame.minY + textPositioningInsets.top - textClippingContainerFrame.minY), size: CGSize(width: maxTextWidth, height: textHeight))
|
||||||
self.updateTextContents()
|
self.updateTextContents()
|
||||||
|
|
||||||
|
// Blend between the raw plain text (source/morph) and the injected rich layout
|
||||||
|
// (settled). `explicitBackgroundSize != nil` means source-morph; nil means settled.
|
||||||
|
let isSettled = explicitBackgroundSize == nil
|
||||||
|
if let richTextPreviewView = self.richTextPreviewView, let richContentSize {
|
||||||
|
let richAlpha: CGFloat = isSettled ? 1.0 : 0.0
|
||||||
|
let plainAlpha: CGFloat = isSettled ? 0.0 : 1.0
|
||||||
|
transition.setAlpha(view: richTextPreviewView, alpha: richAlpha)
|
||||||
|
transition.setAlpha(view: self.textClippingContainer, alpha: plainAlpha)
|
||||||
|
transition.setFrame(view: richTextPreviewView, frame: CGRect(origin: CGPoint(x: richBubbleLeftInset, y: 1.0), size: richContentSize))
|
||||||
|
} else {
|
||||||
|
transition.setAlpha(view: self.textClippingContainer, alpha: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
if let effectIcon = self.effectIcon, let effectIconSize {
|
if let effectIcon = self.effectIcon, let effectIconSize {
|
||||||
if let effectIconView = effectIcon.view {
|
if let effectIconView = effectIcon.view {
|
||||||
var animateIn = false
|
var animateIn = false
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import TopMessageReactions
|
||||||
import ReactionSelectionNode
|
import ReactionSelectionNode
|
||||||
import ChatControllerInteraction
|
import ChatControllerInteraction
|
||||||
import ChatSendAudioMessageContextPreview
|
import ChatSendAudioMessageContextPreview
|
||||||
|
import BrowserUI
|
||||||
|
|
||||||
extension ChatSendMessageEffect {
|
extension ChatSendMessageEffect {
|
||||||
convenience init(_ effect: ChatSendMessageActionSheetController.SendParameters.Effect) {
|
convenience init(_ effect: ChatSendMessageActionSheetController.SendParameters.Effect) {
|
||||||
|
|
@ -211,6 +212,14 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var richTextPreview: ChatSendMessageContextScreenRichTextPreview?
|
||||||
|
if case .customChatContents = selfController.presentationInterfaceState.subject {
|
||||||
|
} else if mediaPreview == nil,
|
||||||
|
let plainText = textInputView.attributedText?.string,
|
||||||
|
let attribute = richMarkdownAttributeIfNeeded(context: selfController.context, text: plainText) {
|
||||||
|
richTextPreview = ChatSendMessageRichTextPreview(context: selfController.context, instantPage: attribute.instantPage)
|
||||||
|
}
|
||||||
|
|
||||||
let controller = makeChatSendMessageActionSheetController(
|
let controller = makeChatSendMessageActionSheetController(
|
||||||
initialData: initialData,
|
initialData: initialData,
|
||||||
context: selfController.context,
|
context: selfController.context,
|
||||||
|
|
@ -286,7 +295,8 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no
|
||||||
},
|
},
|
||||||
reactionItems: (!textInputView.text.isEmpty || mediaPreview != nil) ? effectItems : nil,
|
reactionItems: (!textInputView.text.isEmpty || mediaPreview != nil) ? effectItems : nil,
|
||||||
availableMessageEffects: availableMessageEffects,
|
availableMessageEffects: availableMessageEffects,
|
||||||
isPremium: hasPremium
|
isPremium: hasPremium,
|
||||||
|
richTextPreview: richTextPreview
|
||||||
)
|
)
|
||||||
selfController.sendMessageActionsController = controller
|
selfController.sendMessageActionsController = controller
|
||||||
if layout.isNonExclusive {
|
if layout.isNonExclusive {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import TelegramCore
|
||||||
|
import AccountContext
|
||||||
|
import TelegramPresentationData
|
||||||
|
import ComponentFlow
|
||||||
|
import InstantPageUI
|
||||||
|
import TelegramUIPreferences
|
||||||
|
import ChatSendMessageActionUI
|
||||||
|
|
||||||
|
final class ChatSendMessageRichTextPreview: ChatSendMessageContextScreenRichTextPreview {
|
||||||
|
private let context: AccountContext
|
||||||
|
private let instantPage: InstantPage
|
||||||
|
private let webpage: TelegramMediaWebpage
|
||||||
|
private let pageView: InstantPageV2View
|
||||||
|
|
||||||
|
private var cachedBoundingWidth: CGFloat?
|
||||||
|
private var cachedThemeIdentity: ObjectIdentifier?
|
||||||
|
private var cachedContentSize: CGSize = .zero
|
||||||
|
|
||||||
|
var view: UIView {
|
||||||
|
return self.pageView
|
||||||
|
}
|
||||||
|
|
||||||
|
init(context: AccountContext, instantPage: InstantPage) {
|
||||||
|
self.context = context
|
||||||
|
self.instantPage = instantPage
|
||||||
|
|
||||||
|
let webpage = TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(
|
||||||
|
url: "",
|
||||||
|
displayUrl: "",
|
||||||
|
hash: 0,
|
||||||
|
type: nil,
|
||||||
|
websiteName: nil,
|
||||||
|
title: nil,
|
||||||
|
text: nil,
|
||||||
|
embedUrl: nil,
|
||||||
|
embedType: nil,
|
||||||
|
embedSize: nil,
|
||||||
|
duration: nil,
|
||||||
|
author: nil,
|
||||||
|
isMediaLargeByDefault: nil,
|
||||||
|
imageIsVideoCover: false,
|
||||||
|
image: nil,
|
||||||
|
file: nil,
|
||||||
|
story: nil,
|
||||||
|
attributes: [],
|
||||||
|
instantPage: instantPage
|
||||||
|
)))
|
||||||
|
self.webpage = webpage
|
||||||
|
|
||||||
|
let renderContext = InstantPageV2RenderContext(
|
||||||
|
context: context,
|
||||||
|
webpage: webpage,
|
||||||
|
sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .channel),
|
||||||
|
imageReference: { image in
|
||||||
|
return ImageMediaReference.standalone(media: image)
|
||||||
|
},
|
||||||
|
fileReference: { file in
|
||||||
|
return FileMediaReference.standalone(media: file)
|
||||||
|
},
|
||||||
|
present: { _, _ in },
|
||||||
|
push: { _ in },
|
||||||
|
openUrl: { _ in },
|
||||||
|
baseNavigationController: { return nil }
|
||||||
|
)
|
||||||
|
self.pageView = InstantPageV2View(renderContext: renderContext)
|
||||||
|
self.pageView.isUserInteractionEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(boundingWidth: CGFloat, presentationData: PresentationData, transition: ComponentTransition) -> CGSize {
|
||||||
|
let themeIdentity = ObjectIdentifier(presentationData.theme)
|
||||||
|
if self.cachedBoundingWidth == boundingWidth, self.cachedThemeIdentity == themeIdentity {
|
||||||
|
return self.cachedContentSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combined with MessageItemView's 1pt left border this yields a 10pt left text inset.
|
||||||
|
let pageHorizontalInset: CGFloat = 9.0
|
||||||
|
let isDark = presentationData.theme.overallDarkAppearance
|
||||||
|
let messageTheme = presentationData.theme.chat.message.outgoing
|
||||||
|
let mainColor = messageTheme.accentTextColor
|
||||||
|
|
||||||
|
let codeBlockBackgroundColor: UIColor
|
||||||
|
if isDark {
|
||||||
|
codeBlockBackgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
||||||
|
} else {
|
||||||
|
codeBlockBackgroundColor = mainColor.withMultipliedAlpha(0.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let textCategories = InstantPageTextCategories(
|
||||||
|
kicker: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 0.685), color: messageTheme.primaryTextColor),
|
||||||
|
header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: messageTheme.primaryTextColor),
|
||||||
|
subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: messageTheme.primaryTextColor),
|
||||||
|
paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor),
|
||||||
|
caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: messageTheme.secondaryTextColor),
|
||||||
|
credit: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 13.0, lineSpacingFactor: 1.0), color: messageTheme.secondaryTextColor),
|
||||||
|
table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor),
|
||||||
|
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor)
|
||||||
|
)
|
||||||
|
let pageTheme = InstantPageTheme(
|
||||||
|
type: isDark ? .dark : .light,
|
||||||
|
pageBackgroundColor: .clear,
|
||||||
|
textCategories: textCategories,
|
||||||
|
serif: false,
|
||||||
|
codeBlockBackgroundColor: codeBlockBackgroundColor,
|
||||||
|
linkColor: messageTheme.linkTextColor,
|
||||||
|
textHighlightColor: messageTheme.accentTextColor.withMultipliedAlpha(0.1),
|
||||||
|
linkHighlightColor: messageTheme.linkTextColor.withMultipliedAlpha(0.1),
|
||||||
|
markerColor: UIColor(rgb: 0xfef3bc),
|
||||||
|
panelBackgroundColor: messageTheme.accentControlColor.withMultipliedAlpha(0.1),
|
||||||
|
panelHighlightedBackgroundColor: messageTheme.accentControlColor.withMultipliedAlpha(0.25),
|
||||||
|
panelPrimaryColor: messageTheme.primaryTextColor,
|
||||||
|
panelSecondaryColor: messageTheme.secondaryTextColor,
|
||||||
|
panelAccentColor: messageTheme.accentTextColor,
|
||||||
|
// Preview is always outgoing, so these match the reference bubble's
|
||||||
|
// `isDark || !isIncoming` (always-true) branch.
|
||||||
|
tableBorderColor: messageTheme.accentControlColor.withMultipliedAlpha(0.25),
|
||||||
|
tableHeaderColor: messageTheme.accentControlColor.withMultipliedAlpha(0.1),
|
||||||
|
controlColor: messageTheme.accentControlColor,
|
||||||
|
imageTintColor: nil,
|
||||||
|
overlayPanelColor: isDark ? UIColor(white: 0.0, alpha: 0.13) : UIColor(white: 1.0, alpha: 0.13)
|
||||||
|
)
|
||||||
|
|
||||||
|
let layout = layoutInstantPageV2(
|
||||||
|
webpage: self.webpage,
|
||||||
|
instantPage: self.instantPage,
|
||||||
|
userLocation: .other,
|
||||||
|
boundingWidth: boundingWidth,
|
||||||
|
horizontalInset: pageHorizontalInset,
|
||||||
|
theme: pageTheme,
|
||||||
|
strings: presentationData.strings,
|
||||||
|
dateTimeFormat: presentationData.dateTimeFormat,
|
||||||
|
cachedMessageSyntaxHighlight: nil,
|
||||||
|
expandedDetails: [:],
|
||||||
|
fitToWidth: true
|
||||||
|
)
|
||||||
|
self.pageView.update(layout: layout, theme: pageTheme, animation: .None)
|
||||||
|
// The parent (MessageItemView) owns and sets `pageView`'s frame; `update` only
|
||||||
|
// rebuilds content and reports the size. Rendering is static (.None) — the screen
|
||||||
|
// drives the size/crossfade transition.
|
||||||
|
|
||||||
|
self.cachedBoundingWidth = boundingWidth
|
||||||
|
self.cachedThemeIdentity = themeIdentity
|
||||||
|
self.cachedContentSize = layout.contentSize
|
||||||
|
return layout.contentSize
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue