import Foundation import UIKit import Display import ComponentFlow import TelegramPresentationData import TelegramCore import Postbox import SwiftSignalKit import MultiAnimationRenderer import AnimationCache import AccountContext import TelegramUIPreferences import GenerateStickerPlaceholderImage import EmojiTextAttachmentView import LottieAnimationCache public final class InlineFileIconLayer: MultiAnimationRenderTarget { private final class Arguments { let context: InlineFileIconLayer.Context let userLocation: MediaResourceUserLocation let file: TelegramMediaFile let cache: AnimationCache let renderer: MultiAnimationRenderer let unique: Bool let placeholderColor: UIColor let pointSize: CGSize let pixelSize: CGSize init(context: InlineFileIconLayer.Context, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool, placeholderColor: UIColor, pointSize: CGSize, pixelSize: CGSize) { self.context = context self.userLocation = userLocation self.file = file self.cache = cache self.renderer = renderer self.unique = unique self.placeholderColor = placeholderColor self.pointSize = pointSize self.pixelSize = pixelSize } } public enum Context: Equatable { public final class Custom: Equatable { public let postbox: Postbox public let energyUsageSettings: () -> EnergyUsageSettings public let resolveInlineStickers: ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> public init(postbox: Postbox, energyUsageSettings: @escaping () -> EnergyUsageSettings, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>) { self.postbox = postbox self.energyUsageSettings = energyUsageSettings self.resolveInlineStickers = resolveInlineStickers } public static func ==(lhs: Custom, rhs: Custom) -> Bool { if lhs.postbox !== rhs.postbox { return false } return true } } case account(AccountContext) case custom(Custom) var postbox: Postbox { switch self { case let .account(account): return account.account.postbox case let .custom(custom): return custom.postbox } } var energyUsageSettings: EnergyUsageSettings { switch self { case let .account(account): return account.sharedContext.energyUsageSettings case let .custom(custom): return custom.energyUsageSettings() } } func resolveInlineStickers(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> { switch self { case let .account(account): return account.engine.stickers.resolveInlineStickers(fileIds: fileIds) case let .custom(custom): return custom.resolveInlineStickers(fileIds) } } public static func ==(lhs: Context, rhs: Context) -> Bool { switch lhs { case let .account(lhsContext): if case let .account(rhsContext) = rhs, lhsContext === rhsContext { return true } else { return false } case let .custom(custom): if case .custom(custom) = rhs { return true } else { return false } } } } public static let queue = Queue() public struct Key: Hashable { public var id: Int64 public var index: Int public init(id: Int64, index: Int) { self.id = id self.index = index } } private let arguments: Arguments? private var isDisplayingPlaceholder: Bool = false private var didProcessTintColor: Bool = false public private(set) var file: TelegramMediaFile? private var infoDisposable: Disposable? private var disposable: Disposable? private var fetchDisposable: Disposable? private var loadDisposable: Disposable? private var _contentTintColor: UIColor? public var contentTintColor: UIColor? { get { return self._contentTintColor } set(value) { if self._contentTintColor != value { self._contentTintColor = value } } } private var _dynamicColor: UIColor? public var dynamicColor: UIColor? { get { return self._dynamicColor } set(value) { if self._dynamicColor != value { self._dynamicColor = value } } } private var currentLoopCount: Int = 0 private var isInHierarchyValue: Bool = false public convenience init( context: AccountContext, userLocation: MediaResourceUserLocation, attemptSynchronousLoad: Bool, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, dynamicColor: UIColor? = nil ) { self.init( context: .account(context), userLocation: userLocation, attemptSynchronousLoad: attemptSynchronousLoad, file: file, cache: cache, renderer: renderer, unique: unique, placeholderColor: placeholderColor, pointSize: pointSize, dynamicColor: dynamicColor ) } public init( context: InlineFileIconLayer.Context, userLocation: MediaResourceUserLocation, attemptSynchronousLoad: Bool, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, dynamicColor: UIColor? = nil ) { let scale = min(2.0, UIScreenScale) self.arguments = Arguments( context: context, userLocation: userLocation, file: file, cache: cache, renderer: renderer, unique: unique, placeholderColor: placeholderColor, pointSize: pointSize, pixelSize: CGSize(width: pointSize.width * scale, height: pointSize.height * scale) ) self._dynamicColor = dynamicColor super.init() self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad) } override public init(layer: Any) { self.arguments = nil super.init(layer: layer) } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.loadDisposable?.dispose() self.infoDisposable?.dispose() self.disposable?.dispose() self.fetchDisposable?.dispose() } override public func action(forKey event: String) -> CAAction? { if event == kCAOnOrderIn { self.isInHierarchyValue = true } else if event == kCAOnOrderOut { self.isInHierarchyValue = false } return nullAction } private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) { guard let arguments = self.arguments else { return } if self.file?.fileId == file.fileId { return } self.file = file if attemptSynchronousLoad { if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize) { if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: arguments.pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: arguments.placeholderColor) { self.contents = image.cgImage self.isDisplayingPlaceholder = true } } self.loadAnimation() } else { let isTemplate = file.isCustomTemplateEmoji let pointSize = arguments.pointSize let placeholderColor = arguments.placeholderColor let isThumbnailCancelled = Atomic(value: false) self.loadDisposable = arguments.renderer.loadFirstFrame( target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in if !result { MultiAnimationRendererImpl.firstFrameQueue.async { let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) DispatchQueue.main.async { guard let strongSelf = self, !isThumbnailCancelled.with({ $0 }) else { return } if let image = image { strongSelf.contents = image.cgImage strongSelf.isDisplayingPlaceholder = true } if isFinal { strongSelf.loadAnimation() } } } } else { guard let strongSelf = self else { return } let _ = isThumbnailCancelled.swap(true) strongSelf.loadAnimation() } }) } } private func loadAnimation() { /*guard let arguments = self.arguments else { return } guard let file = self.file else { return } let isTemplate = file.isCustomTemplateEmoji let context = arguments.context if file.isAnimatedSticker || file.isVideoSticker || file.isVideoEmoji { let keyframeOnly = arguments.pixelSize.width >= 120.0 self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly, customColor: isTemplate ? .white : nil)) } else { self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: { options in let dataDisposable = context.postbox.mediaBox.resourceData(file.resource).start(next: { result in guard result.complete else { return } cacheStillSticker(path: result.path, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: isTemplate ? .white : nil) }) let fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: context.postbox, userLocation: arguments.userLocation, fileReference: .customEmoji(media: file), resource: file.resource).start() return ActionDisposable { dataDisposable.dispose() fetchDisposable.dispose() } }) }*/ } override public func updateDisplayPlaceholder(displayPlaceholder: Bool) { if self.isDisplayingPlaceholder == displayPlaceholder { return } self.isDisplayingPlaceholder = displayPlaceholder } override public func transitionToContents(_ contents: AnyObject, didLoop: Bool) { if self.isDisplayingPlaceholder { self.isDisplayingPlaceholder = false if let current = self.contents { let previousLayer = SimpleLayer() previousLayer.contents = current previousLayer.frame = self.frame self.superlayer?.insertSublayer(previousLayer, below: self) previousLayer.opacity = 0.0 previousLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak previousLayer] _ in previousLayer?.removeFromSuperlayer() }) self.contents = contents self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) } else { self.contents = contents self.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } else { self.contents = contents } } }