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

195 lines
8.3 KiB
Swift

import Foundation
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramPresentationData
import UIKit
import WebPBinding
import RLottieBinding
import GZip
import AnimationCache
import EmojiTextAttachmentView
public let sharedReactionStaticImage = Queue(name: "SharedReactionStaticImage", qos: .default)
public func reactionStaticImage(context: AccountContext, animation: TelegramMediaFile, pixelSize: CGSize, queue: Queue) -> Signal<EngineMediaResource.ResourceData, NoError> {
return context.engine.resources.custom(id: "\(animation.resource.id.stringRepresentation):reaction-static-\(pixelSize.width)x\(pixelSize.height)-v10", fetch: EngineMediaResource.Fetch {
return Signal { subscriber in
let fetchDisposable = context.engine.resources.fetch(reference: MediaResourceReference.standalone(resource: animation.resource), userLocation: .other, userContentType: .image).start()
var customColor: UIColor?
if animation.isCustomTemplateEmoji {
customColor = nil
}
let fetchFrame = animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: MediaResourceReference.standalone(resource: animation.resource), type: AnimationCacheAnimationType(file: animation), keyframeOnly: true, customColor: customColor)
class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
let queue: Queue
private let frameReceived: (UIImage) -> Void
init(queue: Queue, frameReceived: @escaping (UIImage) -> Void) {
self.queue = queue
self.frameReceived = frameReceived
}
var isCancelled: Bool {
return false
}
func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) {
if let renderContext = DrawingContext(size: CGSize(width: proposedWidth, height: proposedHeight), scale: 1.0, clear: true) {
let _ = drawingBlock(AnimationCacheItemDrawingSurface(
argb: renderContext.bytes.assumingMemoryBound(to: UInt8.self),
width: Int(renderContext.scaledSize.width),
height: Int(renderContext.scaledSize.height),
bytesPerRow: renderContext.bytesPerRow,
length: renderContext.length
))
if let image = renderContext.generateImage() {
self.frameReceived(image)
}
}
}
func finish() {
}
}
let innerWriter = AnimationCacheItemWriterImpl(queue: queue, frameReceived: { image in
guard let pngData = image.pngData() else {
return
}
let tempFile = EngineTempBox.shared.tempFile(fileName: "image.png")
guard let _ = try? pngData.write(to: URL(fileURLWithPath: tempFile.path)) else {
return
}
subscriber.putNext(.moveTempFile(file: tempFile))
subscriber.putCompletion()
})
let dataDisposable = fetchFrame(AnimationCacheFetchOptions(
size: pixelSize,
writer: innerWriter,
firstFrameOnly: true
))
/*let dataDisposable = context.account.postbox.mediaBox.resourceData(animation.resource).start(next: { data in
if !data.complete {
return
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else {
return
}
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
return
}
guard let instance = LottieInstance(data: unpackedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else {
return
}
instance.renderFrame(with: Int32(instance.frameCount - 1), into: renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(renderContext.size.width * renderContext.scale), height: Int32(renderContext.size.height * renderContext.scale), bytesPerRow: Int32(renderContext.bytesPerRow))
})*/
return ActionDisposable {
fetchDisposable.dispose()
dataDisposable.dispose()
}
}
})
}
public final class ReactionImageNode: ASDisplayNode {
private var disposable: Disposable?
private let size: CGSize
private let isAnimation: Bool
private let iconNode: ASImageNode
public init(context: AccountContext, availableReactions: AvailableReactions?, reaction: MessageReaction.Reaction, displayPixelSize: CGSize) {
self.iconNode = ASImageNode()
var file: TelegramMediaFile?
var animationFile: TelegramMediaFile?
if let availableReactions = availableReactions {
for availableReaction in availableReactions.reactions {
if availableReaction.value == reaction {
file = availableReaction.staticIcon._parse()
animationFile = availableReaction.centerAnimation?._parse()
break
}
}
}
if let animationFile = animationFile {
self.size = animationFile.dimensions?.cgSize ?? displayPixelSize
var displaySize = self.size.aspectFitted(displayPixelSize)
displaySize.width = floor(displaySize.width * 2.0)
displaySize.height = floor(displaySize.height * 2.0)
self.isAnimation = true
super.init()
self.disposable = (reactionStaticImage(context: context, animation: animationFile, pixelSize: CGSize(width: displaySize.width * UIScreenScale, height: displaySize.height * UIScreenScale), queue: sharedReactionStaticImage)
|> deliverOnMainQueue).start(next: { [weak self] data in
guard let strongSelf = self else {
return
}
if data.isComplete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
if let image = UIImage(data: dataValue) {
strongSelf.iconNode.image = image
}
}
}).strict()
} else if let file = file {
self.size = file.dimensions?.cgSize ?? displayPixelSize
self.isAnimation = false
super.init()
self.disposable = (context.engine.resources.data(resource: EngineMediaResource(file.resource))
|> deliverOnMainQueue).start(next: { [weak self] data in
guard let strongSelf = self else {
return
}
if data.isComplete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
if let image = WebP.convert(fromWebP: dataValue) {
strongSelf.iconNode.image = image
}
}
}).strict()
} else {
self.size = displayPixelSize
self.isAnimation = false
super.init()
}
self.addSubnode(self.iconNode)
}
deinit {
self.disposable?.dispose()
}
public func update(size: CGSize) {
var imageSize = self.size.aspectFitted(size)
if self.isAnimation {
imageSize.width *= 2.0
imageSize.height *= 2.0
}
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)
}
}