Files
isaac d1aa0db537 Postbox -> TelegramEngine waves 46-93 (squashed)
Squash of 63 commits spanning waves 46-93 (plus interspersed docs commits)
of the gradual Postbox->TelegramEngine consumer-side migration.

Scope: 139 files changed, 2123 insertions(+), 452 deletions(-).

## Themes by wave-block

**Waves 46-58 — Peer field migrations + facade additions**
Foundational EnginePeer convenience init additions (PeerReference, RenderedPeer,
SelectivePrivacyPeer). Multiple `peer: Peer` field migrations across PeerInfo,
ChatList, and SettingsUI components.

**Waves 59-73 — peer field cascade + EnginePeer wrap drops**
Series of single- to two-file peer-field migrations; consumer-side wrap
removal (`EnginePeer(peer)` -> direct EnginePeer use); `as? TelegramUser`
cast conversion to `case let .user(...)` enum match. Wave 64: RenderedPeer
convenience init. Wave 68: SelectivePrivacyPeer convenience init.

**Waves 74-83 — controller-Node bridge cleanup + small migrations**
Wave-71 shadow-pattern cleanup at controller->Node bridges. Migrations of
ChatRecentActionsController.peer (74), PeerInfoMember (75), MentionChatInputPanelItem
(76), PassportUI SecureIdAuthController (77), AccountWithInfo + ShareController
(78), peerInputActivitiesPromise (79), InactiveChannel (80), BlockedPeers (81),
openHashtag resolveSignal (82), NotificationExceptionsList (83).

**Waves 84-90 — TelegramEngine.Resources facade migrations**
Per-method Shape-A/B sweeps converting `<ctx>.account.postbox.mediaBox.X(...)`
to `<ctx>.engine.resources.X(...)`. Wave 90 was a single-commit big sweep:
40 fetchedMediaResource sites in 25 files migrated to engine.resources.fetch
facade in one atomic pass with first-pass-clean build.

Methods covered: storeResourceData, completedResourcePath, cancelInteractiveResourceFetch,
resourceRangesStatus, resourceStatus, fetch (fetchedMediaResource).

**Waves 91-92 — additional type migrations**
Wave 91: ItemListWebsiteItem.peer + RecentSessionsController enum-case payload
+ openWebSession callback Peer? -> EnginePeer?.
Wave 92: ChatListController StateHolder.EntryContext status type
MediaResourceStatus -> EngineMediaResource.FetchStatus.

**Wave 93 — speculative `import Postbox` drop sweep**
Drop import from 7 wave-touched files where it became unused; restore in 5
files where bare PeerId/Message/MediaId/StoryId references escaped the
pre-flight regex. Includes one MediaId(...) -> EngineMedia.Id(...) swap in
InAppPurchaseManager to unlock its import drop.

## Build state

Final state at squash: clean Telegram/Telegram build at debug_sim_arm64.

## Persistent-state notes

- Pre-existing WIP unchanged across the squashed range:
  - build-system/bazel-rules/sourcekit-bazel-bsp submodule marker
  - Untracked: build-system/tulsi/, submodules/TgVoip/, third-party/libx264/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:48:15 +04:00

524 lines
28 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import StickerResources
import PhotoResources
import TelegramStringFormatting
import AnimatedCountLabelNode
import AnimatedNavigationStripeNode
import ContextUI
import RadialStatusNode
import TextFormat
import TextNodeWithEntities
import TranslateUI
private enum PinnedMessageAnimation {
case slideToTop
case slideToBottom
}
final class ChatAdPanelNode: ASDisplayNode {
private let context: AccountContext
private let action: (EngineMessage) -> Void
private let contextAction: (EngineMessage, ASDisplayNode, ContextGesture?) -> Void
private let close: () -> Void
private(set) var message: EngineMessage?
private let tapButton: HighlightTrackingButtonNode
private let contextContainer: ContextControllerSourceNode
private let clippingContainer: ASDisplayNode
private let contentContainer: ASDisplayNode
private let contentTextContainer: ASDisplayNode
private let adNode: TextNode
private let titleNode: TextNode
private let textNode: TextNodeWithEntities
private let removeButtonNode: HighlightTrackingButtonNode
private let removeBackgroundNode: ASImageNode
private let removeTextNode: ImmediateTextNode
private let closeButton: HighlightableButtonNode
private let imageNode: TransformImageNode
private let imageNodeContainer: ASDisplayNode
private var currentLayout: (CGFloat, CGFloat, CGFloat)?
private var previousMediaReference: AnyMediaReference?
private let fetchDisposable = MetaDisposable()
init(
context: AccountContext,
action: @escaping (EngineMessage) -> Void,
contextAction: @escaping (EngineMessage, ASDisplayNode, ContextGesture?) -> Void,
close: @escaping () -> Void
) {
self.context = context
self.action = action
self.contextAction = contextAction
self.close = close
self.tapButton = HighlightTrackingButtonNode()
self.contextContainer = ContextControllerSourceNode()
self.clippingContainer = ASDisplayNode()
self.clippingContainer.clipsToBounds = true
self.contentContainer = ASDisplayNode()
self.contentTextContainer = ASDisplayNode()
self.adNode = TextNode()
self.adNode.displaysAsynchronously = false
self.adNode.isUserInteractionEnabled = false
self.removeButtonNode = HighlightTrackingButtonNode()
self.removeBackgroundNode = ASImageNode()
self.removeTextNode = ImmediateTextNode()
self.removeTextNode.displaysAsynchronously = false
self.removeTextNode.isUserInteractionEnabled = false
self.titleNode = TextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.isUserInteractionEnabled = false
self.textNode = TextNodeWithEntities()
self.textNode.textNode.displaysAsynchronously = false
self.textNode.textNode.isUserInteractionEnabled = false
self.imageNode = TransformImageNode()
self.imageNode.contentAnimations = [.subsequentUpdates]
self.imageNodeContainer = ASDisplayNode()
self.closeButton = HighlightableButtonNode()
self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
self.closeButton.displaysAsynchronously = false
super.init()
self.addSubnode(self.contextContainer)
self.contextContainer.addSubnode(self.clippingContainer)
self.clippingContainer.addSubnode(self.contentContainer)
self.contentTextContainer.addSubnode(self.titleNode)
self.contentTextContainer.addSubnode(self.adNode)
self.contentTextContainer.addSubnode(self.textNode.textNode)
self.contentContainer.addSubnode(self.contentTextContainer)
self.imageNodeContainer.addSubnode(self.imageNode)
self.contentContainer.addSubnode(self.imageNodeContainer)
self.tapButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside])
self.tapButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.adNode.layer.removeAnimation(forKey: "opacity")
strongSelf.adNode.alpha = 0.4
strongSelf.titleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.titleNode.alpha = 0.4
strongSelf.textNode.textNode.layer.removeAnimation(forKey: "opacity")
strongSelf.textNode.textNode.alpha = 0.4
strongSelf.imageNode.layer.removeAnimation(forKey: "opacity")
strongSelf.imageNode.alpha = 0.4
strongSelf.removeTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.removeTextNode.alpha = 0.4
strongSelf.removeBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.removeBackgroundNode.alpha = 0.4
} else {
strongSelf.adNode.alpha = 1.0
strongSelf.adNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.titleNode.alpha = 1.0
strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.textNode.textNode.alpha = 1.0
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.imageNode.alpha = 1.0
strongSelf.imageNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.removeTextNode.alpha = 1.0
strongSelf.removeTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.removeBackgroundNode.alpha = 1.0
strongSelf.removeBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.contextContainer.addSubnode(self.tapButton)
self.contextContainer.addSubnode(self.removeBackgroundNode)
self.contextContainer.addSubnode(self.removeTextNode)
self.contextContainer.addSubnode(self.removeButtonNode)
self.removeButtonNode.addTarget(self, action: #selector(self.removePressed), forControlEvents: [.touchUpInside])
self.removeButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.removeTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.removeTextNode.alpha = 0.4
strongSelf.removeBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.removeBackgroundNode.alpha = 0.4
} else {
strongSelf.removeTextNode.alpha = 1.0
strongSelf.removeTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.removeBackgroundNode.alpha = 1.0
strongSelf.removeBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.contextContainer.activated = { [weak self] gesture, _ in
guard let self, let message = self.message else {
return
}
self.contextAction(message, self.contextContainer, gesture)
}
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside])
self.addSubnode(self.closeButton)
}
deinit {
self.fetchDisposable.dispose()
}
private var theme: PresentationTheme?
@objc private func closePressed() {
/*if self.context.isPremium, let adAttribute = self.message?.adAttribute {
self.controllerInteraction?.removeAd(adAttribute.opaqueId)
} else {
self.controllerInteraction?.openNoAdsDemo()
}*/
self.close()
}
func updateLayout(width: CGFloat, theme: PresentationTheme, strings: PresentationStrings, info: AdPanelHeaderPanelComponent.Info, transition: ContainedViewLayoutTransition) -> CGFloat {
let leftInset: CGFloat = 0.0
let rightInset: CGFloat = 0.0
self.message = info.message
if self.theme !== theme {
self.theme = theme
self.removeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 15.0, color: theme.chat.inputPanel.panelControlColor.withMultipliedAlpha(0.1))
self.removeTextNode.attributedText = NSAttributedString(string: strings.Chat_BotAd_WhatIsThis, font: Font.regular(11.0), textColor: theme.chat.inputPanel.panelControlColor)
self.closeButton.setImage(generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.chat.inputPanel.panelControlColor.cgColor)
context.setLineWidth(1.33)
context.setLineCap(.round)
context.move(to: CGPoint(x: 1.0, y: 1.0))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - 1.0, y: 1.0))
context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0))
context.strokePath()
}), for: [])
}
self.contextContainer.isGestureEnabled = false
let panelHeight: CGFloat
var hasCloseButton = true
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 })
panelHeight = self.enqueueTransition(width: width, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: nil, message: info.message, theme: theme, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: false, isReplyThread: false, translateToLanguage: nil)
hasCloseButton = info.message.media.isEmpty
self.contextContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
self.clippingContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
let contentRightInset: CGFloat = 14.0 + rightInset
let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0))
self.closeButton.frame = CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize)
self.closeButton.isHidden = !hasCloseButton
self.currentLayout = (width, leftInset, rightInset)
return panelHeight
}
private func enqueueTransition(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, message: EngineMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool, translateToLanguage: String?) -> CGFloat {
var animationTransition: ContainedViewLayoutTransition = .immediate
if let animation = animation {
animationTransition = .animated(duration: 0.2, curve: .easeInOut)
if let copyView = self.textNode.textNode.view.snapshotView(afterScreenUpdates: false) {
let offset: CGFloat
switch animation {
case .slideToTop:
offset = -10.0
case .slideToBottom:
offset = 10.0
}
copyView.frame = self.textNode.textNode.frame
self.textNode.textNode.view.superview?.addSubview(copyView)
copyView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.2, removeOnCompletion: false, additive: true)
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in
copyView?.removeFromSuperview()
})
self.textNode.textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -offset), to: CGPoint(), duration: 0.2, additive: true)
self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
let makeAdLayout = TextNode.asyncLayout(self.adNode)
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let imageNodeLayout = self.imageNode.asyncLayout()
let previousMediaReference = self.previousMediaReference
let context = self.context
let contentLeftInset: CGFloat = leftInset + 18.0
let contentRightInset: CGFloat = rightInset + 9.0
var textRightInset: CGFloat = 0.0
var updatedMediaReference: AnyMediaReference?
var imageDimensions: CGSize?
if !message._asMessage().containsSecretMedia {
for media in message.media {
if let image = media as? TelegramMediaImage {
updatedMediaReference = .message(message: MessageReference(message._asMessage()), media: image)
if let representation = largestRepresentationForPhoto(image) {
imageDimensions = representation.dimensions.cgSize
}
break
} else if let file = media as? TelegramMediaFile {
updatedMediaReference = .message(message: MessageReference(message._asMessage()), media: file)
if !file.isInstantVideo && !file.isSticker, let representation = largestImageRepresentation(file.previewRepresentations) {
imageDimensions = representation.dimensions.cgSize
} else if file.isAnimated, let dimensions = file.dimensions {
imageDimensions = dimensions.cgSize
}
break
} else if let paidContent = media as? TelegramMediaPaidContent, let firstMedia = paidContent.extendedMedia.first {
switch firstMedia {
case let .preview(dimensions, immediateThumbnailData, _):
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
if let dimensions {
imageDimensions = dimensions.cgSize
}
updatedMediaReference = .standalone(media: thumbnailMedia)
case let .full(fullMedia):
updatedMediaReference = .message(message: MessageReference(message._asMessage()), media: fullMedia)
if let image = fullMedia as? TelegramMediaImage {
if let representation = largestRepresentationForPhoto(image) {
imageDimensions = representation.dimensions.cgSize
}
break
} else if let file = fullMedia as? TelegramMediaFile {
if let dimensions = file.dimensions {
imageDimensions = dimensions.cgSize
}
break
}
}
}
}
}
let imageBoundingSize = CGSize(width: 48.0, height: 48.0)
var applyImage: (() -> Void)?
if let imageDimensions {
applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 10.0), imageSize: imageDimensions.aspectFilled(imageBoundingSize), boundingSize: imageBoundingSize, intrinsicInsets: UIEdgeInsets()))
textRightInset += imageBoundingSize.width + 18.0
} else {
textRightInset = 27.0
}
var mediaUpdated = false
if let updatedMediaReference = updatedMediaReference, let previousMediaReference = previousMediaReference {
mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media)
} else if (updatedMediaReference != nil) != (previousMediaReference != nil) {
mediaUpdated = true
}
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var updatedFetchMediaSignal: Signal<FetchResourceSourceType, FetchResourceError>?
if mediaUpdated {
if let updatedMediaReference = updatedMediaReference, imageDimensions != nil {
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
if imageReference.media.representations.isEmpty {
updateImageSignal = chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, ignoreFullSize: true, synchronousLoad: true)
} else {
updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: false)
}
} else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) {
if fileReference.media.isAnimatedSticker {
let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512)
updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)))
updatedFetchMediaSignal = context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media))
} else if fileReference.media.isVideo || fileReference.media.isAnimated {
updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, blurred: false)
} else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) {
updateImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .peer(message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation)
}
}
} else {
updateImageSignal = .single({ _ in return nil })
}
}
let (adLayout, adApply) = makeAdLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Chat_BotAd_Title, font: Font.semibold(14.0), textColor: theme.chat.inputPanel.panelControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width, height: .greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: .zero))
let titleConstrainedSize = CGSize(width: width - contentLeftInset - contentRightInset - textRightInset - adLayout.size.width - 90.0, height: CGFloat.greatestFiniteMagnitude)
let textConstrainedSize = CGSize(width: width - contentLeftInset - contentRightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude)
var titleText: String = ""
if let author = message.author {
titleText = author.compactDisplayTitle
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleText, font: Font.semibold(14.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: titleConstrainedSize, alignment: .natural, cutout: nil, insets: .zero))
let (textString, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId)
let messageText: NSAttributedString
let textFont = Font.regular(14.0)
if isText {
var text = message.text
var messageEntities = message._asMessage().textEntitiesAttribute?.entities ?? []
if let translateToLanguage = translateToLanguage, !text.isEmpty {
for attribute in message.attributes {
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
text = attribute.text
messageEntities = attribute.entities
break
}
}
}
let entities = messageEntities.filter { entity in
switch entity.type {
case .CustomEmoji:
return true
default:
return false
}
}
let textColor = theme.chat.inputPanel.primaryTextColor
if entities.count > 0 {
messageText = stringWithAppliedEntities(trimToLineCount(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, message: message._asMessage())
} else {
messageText = NSAttributedString(string: foldLineBreaks(text), font: textFont, textColor: textColor)
}
} else {
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 (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: .zero))
var panelHeight: CGFloat = 0.0
if let _ = imageDimensions {
panelHeight = 9.0 + imageBoundingSize.height + 9.0
}
var textHeight: CGFloat
var titleOnSeparateLine = false
if textLayout.numberOfLines == 1 || contentLeftInset + adLayout.size.width + 2.0 + titleLayout.size.width > width - contentRightInset - textRightInset {
textHeight = adLayout.size.height + titleLayout.size.height + textLayout.size.height + 15.0
titleOnSeparateLine = true
} else {
textHeight = titleLayout.size.height + textLayout.size.height + 15.0
}
panelHeight = max(panelHeight, textHeight)
Queue.mainQueue().async {
let _ = adApply()
let _ = titleApply()
let textArguments = TextNodeWithEntities.Arguments(
context: self.context,
cache: self.context.animationCache,
renderer: self.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
attemptSynchronous: false
)
let _ = textApply(textArguments)
self.previousMediaReference = updatedMediaReference
let textContainerFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: width, height: panelHeight))
animationTransition.updateFrameAdditive(node: self.contentTextContainer, frame: textContainerFrame)
let removeTextSize = self.removeTextNode.updateLayout(CGSize(width: width, height: .greatestFiniteMagnitude))
if titleOnSeparateLine {
self.adNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 9.0), size: adLayout.size)
self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 26.0), size: titleLayout.size)
self.textNode.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 43.0), size: textLayout.size)
self.removeTextNode.frame = CGRect(origin: CGPoint(x: contentLeftInset + adLayout.size.width + 8.0, y: 11.0 - UIScreenPixel), size: removeTextSize)
self.removeBackgroundNode.frame = self.removeTextNode.frame.insetBy(dx: -5.0, dy: -1.0)
self.removeButtonNode.frame = self.removeBackgroundNode.frame
} else {
self.adNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 9.0), size: adLayout.size)
self.titleNode.frame = CGRect(origin: CGPoint(x: adLayout.size.width + 2.0, y: 9.0), size: titleLayout.size)
self.textNode.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 26.0), size: textLayout.size)
self.removeTextNode.frame = CGRect(origin: CGPoint(x: contentLeftInset + adLayout.size.width + 2.0 + titleLayout.size.width + 8.0, y: 11.0 - UIScreenPixel), size: removeTextSize)
self.removeBackgroundNode.frame = self.removeTextNode.frame.insetBy(dx: -5.0, dy: -1.0)
self.removeButtonNode.frame = self.removeBackgroundNode.frame
}
self.textNode.visibilityRect = CGRect.infinite
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: width - contentRightInset - imageBoundingSize.width, y: 9.0), size: imageBoundingSize)
self.imageNode.frame = CGRect(origin: CGPoint(), size: imageBoundingSize)
if let applyImage = applyImage {
applyImage()
animationTransition.updateSublayerTransformScale(node: self.imageNodeContainer, scale: 1.0)
animationTransition.updateAlpha(node: self.imageNodeContainer, alpha: 1.0, beginWithCurrentState: true)
} else {
animationTransition.updateSublayerTransformScale(node: self.imageNodeContainer, scale: 0.1)
animationTransition.updateAlpha(node: self.imageNodeContainer, alpha: 0.0, beginWithCurrentState: true)
}
if let updateImageSignal = updateImageSignal {
self.imageNode.setSignal(updateImageSignal)
}
if let updatedFetchMediaSignal = updatedFetchMediaSignal {
self.fetchDisposable.set(updatedFetchMediaSignal.startStrict())
}
}
return panelHeight
}
@objc func tapped() {
guard let message = self.message else {
return
}
self.action(message)
}
@objc func removePressed() {
guard let message = self.message else {
return
}
self.contextAction(message, self.contextContainer, nil)
}
}