mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-05-21 18:20:41 +00:00
d1aa0db537
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>
195 lines
8.3 KiB
Swift
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)
|
|
}
|
|
}
|