diff --git a/submodules/StickerResources/Sources/StickerResources.swift b/submodules/StickerResources/Sources/StickerResources.swift index 97735c297f..3063bc9cad 100644 --- a/submodules/StickerResources/Sources/StickerResources.swift +++ b/submodules/StickerResources/Sources/StickerResources.swift @@ -9,7 +9,7 @@ import Tuples import ImageBlur import FastBlur -private func imageFromAJpeg(data: Data) -> (UIImage, UIImage)? { +public func imageFromAJpeg(data: Data) -> (UIImage, UIImage)? { if let (colorData, alphaData) = data.withUnsafeBytes({ bytes -> (Data, Data)? in var colorSize: Int32 = 0 memcpy(&colorSize, bytes.baseAddress, 4) diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 6f7b9677f9..1416d25a2a 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -547,7 +547,9 @@ public final class EmojiTextAttachmentView: UIView { public var isActive: Bool = true { didSet { - + if self.isActive != oldValue { + self.contentLayer.isVisibleForAnimations = self.isActive + } } } diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift index 76feb978c6..8b13789179 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift @@ -1,886 +1 @@ -import Foundation -import UIKit -import SwiftSignalKit -import Display -import AnimationCache -import Accelerate -import simd -private func alignUp(size: Int, align: Int) -> Int { - precondition(((align - 1) & align) == 0, "Align must be a power of two") - - let alignmentMask = align - 1 - return (size + alignmentMask) & ~alignmentMask -} - -private extension Float { - func remap(fromLow: Float, fromHigh: Float, toLow: Float, toHigh: Float) -> Float { - guard (fromHigh - fromLow) != 0.0 else { - return 0.0 - } - return toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) - } -} - -private func makePipelineState(device: MTLDevice, library: MTLLibrary, vertexProgram: String, fragmentProgram: String) -> MTLRenderPipelineState? { - guard let loadedVertexProgram = library.makeFunction(name: vertexProgram) else { - return nil - } - guard let loadedFragmentProgram = library.makeFunction(name: fragmentProgram) else { - return nil - } - - let pipelineStateDescriptor = MTLRenderPipelineDescriptor() - pipelineStateDescriptor.vertexFunction = loadedVertexProgram - pipelineStateDescriptor.fragmentFunction = loadedFragmentProgram - pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm - guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineStateDescriptor) else { - return nil - } - - return pipelineState -} - -@available(iOS 13.0, *) -public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { - private final class LoadFrameTask { - let task: () -> () -> Void - - init(task: @escaping () -> () -> Void) { - self.task = task - } - } - - private final class TargetReference { - let id: Int64 - weak var value: MultiAnimationRenderTarget? - - init(_ value: MultiAnimationRenderTarget) { - self.value = value - self.id = value.id - } - } - - private final class TextureStoragePool { - struct Parameters { - let width: Int - let height: Int - let format: TextureStorage.Content.Format - } - - let parameters: Parameters - private var items: [TextureStorage.Content] = [] - private var cleanupTimer: Foundation.Timer? - private var lastTakeTimestamp: Double = 0.0 - - init(width: Int, height: Int, format: TextureStorage.Content.Format) { - self.parameters = Parameters(width: width, height: height, format: format) - - let cleanupTimer = Foundation.Timer(timeInterval: 2.0, repeats: true, block: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.collect() - }) - self.cleanupTimer = cleanupTimer - RunLoop.main.add(cleanupTimer, forMode: .common) - } - - deinit { - self.cleanupTimer?.invalidate() - } - - private func collect() { - let timestamp = CFAbsoluteTimeGetCurrent() - if timestamp - self.lastTakeTimestamp < 1.0 { - return - } - if self.items.count > 32 { - autoreleasepool { - var remainingItems: [Unmanaged] = [] - while self.items.count > 32 { - let item = self.items.removeLast() - remainingItems.append(Unmanaged.passRetained(item)) - } - DispatchQueue.global().async { - autoreleasepool { - for item in remainingItems { - item.release() - } - } - } - } - } - } - - func recycle(content: TextureStorage.Content) { - self.items.append(content) - } - - func take() -> TextureStorage? { - if self.items.isEmpty { - self.lastTakeTimestamp = CFAbsoluteTimeGetCurrent() - return nil - } - return TextureStorage(pool: self, content: self.items.removeLast()) - } - - static func takeNew(device: MTLDevice, parameters: Parameters, pool: TextureStoragePool) -> TextureStorage? { - guard let content = TextureStorage.Content(device: device, width: parameters.width, height: parameters.height, format: parameters.format) else { - return nil - } - return TextureStorage(pool: pool, content: content) - } - } - - private final class TextureStorage { - final class Content { - enum Format { - case bgra - case r - } - - let buffer: MTLBuffer? - - let width: Int - let height: Int - let bytesPerRow: Int - let texture: MTLTexture - - static func rowAlignment(device: MTLDevice, format: Format) -> Int { - let pixelFormat: MTLPixelFormat - switch format { - case .bgra: - pixelFormat = .bgra8Unorm - case .r: - pixelFormat = .r8Unorm - } - return device.minimumLinearTextureAlignment(for: pixelFormat) - } - - init?(device: MTLDevice, width: Int, height: Int, format: Format) { - let bytesPerPixel: Int - let pixelFormat: MTLPixelFormat - switch format { - case .bgra: - bytesPerPixel = 4 - pixelFormat = .bgra8Unorm - case .r: - bytesPerPixel = 1 - pixelFormat = .r8Unorm - } - let pixelRowAlignment = Content.rowAlignment(device: device, format: format) - let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment) - - self.width = width - self.height = height - self.bytesPerRow = bytesPerRow - - #if targetEnvironment(simulator) - let textureDescriptor = MTLTextureDescriptor() - textureDescriptor.textureType = .type2D - textureDescriptor.pixelFormat = pixelFormat - textureDescriptor.width = width - textureDescriptor.height = height - textureDescriptor.usage = [.shaderRead] - textureDescriptor.storageMode = .shared - - guard let texture = device.makeTexture(descriptor: textureDescriptor) else { - return nil - } - self.buffer = nil - #else - guard let buffer = device.makeBuffer(length: bytesPerRow * height, options: MTLResourceOptions.storageModeShared) else { - return nil - } - self.buffer = buffer - - let textureDescriptor = MTLTextureDescriptor() - textureDescriptor.textureType = .type2D - textureDescriptor.pixelFormat = pixelFormat - textureDescriptor.width = width - textureDescriptor.height = height - textureDescriptor.usage = [.shaderRead] - textureDescriptor.storageMode = buffer.storageMode - - guard let texture = buffer.makeTexture(descriptor: textureDescriptor, offset: 0, bytesPerRow: bytesPerRow) else { - return nil - } - #endif - - self.texture = texture - } - - func replace(rgbaData: Data, width: Int, height: Int, bytesPerRow: Int) { - if width != self.width || height != self.height { - assert(false, "Image size does not match") - return - } - let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), size: MTLSize(width: width, height: height, depth: 1)) - - if let buffer = self.buffer, self.bytesPerRow == bytesPerRow { - assert(bytesPerRow * height <= rgbaData.count) - - rgbaData.withUnsafeBytes { bytes in - let _ = memcpy(buffer.contents(), bytes.baseAddress!, bytesPerRow * height) - } - } else { - rgbaData.withUnsafeBytes { bytes in - self.texture.replace(region: region, mipmapLevel: 0, withBytes: bytes.baseAddress!, bytesPerRow: bytesPerRow) - } - } - } - } - - private weak var pool: TextureStoragePool? - let content: Content - private var isInvalidated: Bool = false - - init(pool: TextureStoragePool, content: Content) { - self.pool = pool - self.content = content - } - - deinit { - if !self.isInvalidated { - self.pool?.recycle(content: self.content) - } - } - } - - private final class Frame { - let duration: Double - let textureY: TextureStorage - let textureU: TextureStorage - let textureV: TextureStorage - let textureA: TextureStorage - - var remainingDuration: Double - - init?(device: MTLDevice, textureY: TextureStorage, textureU: TextureStorage, textureV: TextureStorage, textureA: TextureStorage, data: AnimationCacheItemFrame, duration: Double) { - self.duration = duration - self.remainingDuration = duration - - self.textureY = textureY - self.textureU = textureU - self.textureV = textureV - self.textureA = textureA - - switch data.format { - case .rgba: - return nil - case let .yuva(y, u, v, a): - self.textureY.content.replace(rgbaData: y.data, width: y.width, height: y.height, bytesPerRow: y.bytesPerRow) - self.textureU.content.replace(rgbaData: u.data, width: u.width, height: u.height, bytesPerRow: u.bytesPerRow) - self.textureV.content.replace(rgbaData: v.data, width: v.width, height: v.height, bytesPerRow: v.bytesPerRow) - self.textureA.content.replace(rgbaData: a.data, width: a.width, height: a.height, bytesPerRow: a.bytesPerRow) - } - } - } - - private final class ItemContext { - static let queue = Queue(name: "MultiAnimationMetalRendererImpl", qos: .default) - - private let cache: AnimationCache - private let stateUpdated: () -> Void - - private var disposable: Disposable? - private var item: AnimationCacheItem? - - private(set) var currentFrame: Frame? - private var isLoadingFrame: Bool = false - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - self.stateUpdated() - } - } - } - - var targets: [TargetReference] = [] - var slotIndex: Int - private let preferredRowAlignment: Int - - init(slotIndex: Int, preferredRowAlignment: Int, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, stateUpdated: @escaping () -> Void) { - self.slotIndex = slotIndex - self.preferredRowAlignment = preferredRowAlignment - self.cache = cache - self.stateUpdated = stateUpdated - - self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - strongSelf.item = result.item - strongSelf.updateIsPlaying() - - if result.item == nil { - for target in strongSelf.targets { - if let target = target.value { - target.updateDisplayPlaceholder(displayPlaceholder: true) - } - } - } - } - }) - } - - deinit { - self.disposable?.dispose() - } - - func updateIsPlaying() { - var isPlaying = true - if self.item == nil { - isPlaying = false - } - - var shouldBeAnimating = false - for target in self.targets { - if let target = target.value { - if target.shouldBeAnimating { - shouldBeAnimating = true - break - } - } - } - if !shouldBeAnimating { - isPlaying = false - } - - self.isPlaying = isPlaying - } - - func animationTick(device: MTLDevice, texturePoolFullPlane: TextureStoragePool, texturePoolHalfPlane: TextureStoragePool, advanceTimestamp: Double) -> LoadFrameTask? { - return self.update(device: device, texturePoolFullPlane: texturePoolFullPlane, texturePoolHalfPlane: texturePoolHalfPlane, advanceTimestamp: advanceTimestamp) - } - - private func update(device: MTLDevice, texturePoolFullPlane: TextureStoragePool, texturePoolHalfPlane: TextureStoragePool, advanceTimestamp: Double) -> LoadFrameTask? { - guard let item = self.item else { - return nil - } - - if let currentFrame = self.currentFrame, !self.isLoadingFrame { - currentFrame.remainingDuration -= advanceTimestamp - } - - var frameAdvance: AnimationCacheItem.Advance? - if !self.isLoadingFrame { - if let currentFrame = self.currentFrame, advanceTimestamp > 0.0 { - let divisionFactor = advanceTimestamp / currentFrame.remainingDuration - let wholeFactor = round(divisionFactor) - if abs(wholeFactor - divisionFactor) < 0.005 { - currentFrame.remainingDuration = 0.0 - frameAdvance = .frames(Int(wholeFactor)) - } else { - currentFrame.remainingDuration -= advanceTimestamp - if currentFrame.remainingDuration <= 0.0 { - frameAdvance = .duration(currentFrame.duration + max(0.0, -currentFrame.remainingDuration)) - } - } - } else if self.currentFrame == nil { - frameAdvance = .frames(1) - } - } - - if let frameAdvance = frameAdvance, !self.isLoadingFrame { - self.isLoadingFrame = true - - let fullParameters = texturePoolFullPlane.parameters - let halfParameters = texturePoolHalfPlane.parameters - - let readyTextureY = texturePoolFullPlane.take() - let readyTextureU = texturePoolHalfPlane.take() - let readyTextureV = texturePoolHalfPlane.take() - let readyTextureA = texturePoolFullPlane.take() - let preferredRowAlignment = self.preferredRowAlignment - - return LoadFrameTask(task: { [weak self] in - let frame = item.advance(advance: frameAdvance, requestedFormat: .yuva(rowAlignment: preferredRowAlignment))?.frame - - let textureY = readyTextureY ?? TextureStoragePool.takeNew(device: device, parameters: fullParameters, pool: texturePoolFullPlane) - let textureU = readyTextureU ?? TextureStoragePool.takeNew(device: device, parameters: halfParameters, pool: texturePoolHalfPlane) - let textureV = readyTextureV ?? TextureStoragePool.takeNew(device: device, parameters: halfParameters, pool: texturePoolHalfPlane) - let textureA = readyTextureA ?? TextureStoragePool.takeNew(device: device, parameters: fullParameters, pool: texturePoolFullPlane) - - var currentFrame: Frame? - if let frame = frame, let textureY = textureY, let textureU = textureU, let textureV = textureV, let textureA = textureA { - currentFrame = Frame(device: device, textureY: textureY, textureU: textureU, textureV: textureV, textureA: textureA, data: frame, duration: frame.duration) - } - - return { - guard let strongSelf = self else { - return - } - - strongSelf.isLoadingFrame = false - - if let currentFrame = currentFrame { - strongSelf.currentFrame = currentFrame - } - } - }) - } - - return nil - } - } - - private final class SurfaceLayer: CAMetalLayer { - private let cellSize: CGSize - private let stateUpdated: () -> Void - - private let metalDevice: MTLDevice - private let commandQueue: MTLCommandQueue - private let renderPipelineState: MTLRenderPipelineState - - private let texturePoolFullPlane: TextureStoragePool - private let texturePoolHalfPlane: TextureStoragePool - - private let preferredRowAlignment: Int - - private let slotCount: Int - private let slotsX: Int - private let slotsY: Int - private var itemContexts: [String: ItemContext] = [:] - private var slotToItemId: [String?] - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - self.stateUpdated() - } - } - } - - public init(cellSize: CGSize, stateUpdated: @escaping () -> Void) { - self.cellSize = cellSize - self.stateUpdated = stateUpdated - - let resolutionX = max(1, (1024 / Int(cellSize.width))) * Int(cellSize.width) - let resolutionY = max(1, (1024 / Int(cellSize.height))) * Int(cellSize.height) - self.slotsX = resolutionX / Int(cellSize.width) - self.slotsY = resolutionY / Int(cellSize.height) - let drawableSize = CGSize(width: cellSize.width * CGFloat(self.slotsX), height: cellSize.height * CGFloat(self.slotsY)) - - self.slotCount = (Int(drawableSize.width) / Int(cellSize.width)) * (Int(drawableSize.height) / Int(cellSize.height)) - self.slotToItemId = (0 ..< self.slotCount).map { _ in nil } - - self.metalDevice = MTLCreateSystemDefaultDevice()! - self.commandQueue = self.metalDevice.makeCommandQueue()! - - let mainBundle = Bundle(for: MultiAnimationMetalRendererImpl.self) - - guard let path = mainBundle.path(forResource: "MultiAnimationRendererBundle", ofType: "bundle") else { - preconditionFailure() - } - guard let bundle = Bundle(path: path) else { - preconditionFailure() - } - guard let defaultLibrary = try? self.metalDevice.makeDefaultLibrary(bundle: bundle) else { - preconditionFailure() - } - - self.renderPipelineState = makePipelineState(device: self.metalDevice, library: defaultLibrary, vertexProgram: "multiAnimationVertex", fragmentProgram: "multiAnimationFragment")! - - self.texturePoolFullPlane = TextureStoragePool(width: Int(self.cellSize.width), height: Int(self.cellSize.height), format: .r) - self.texturePoolHalfPlane = TextureStoragePool(width: Int(self.cellSize.width) / 2, height: Int(self.cellSize.height) / 2, format: .r) - - self.preferredRowAlignment = TextureStorage.Content.rowAlignment(device: self.metalDevice, format: .r) - - super.init() - - self.device = self.metalDevice - self.maximumDrawableCount = 2 - //self.metalLayer.presentsWithTransaction = true - self.contentsScale = 1.0 - - self.drawableSize = drawableSize - - self.pixelFormat = .bgra8Unorm - self.framebufferOnly = true - self.allowsNextDrawableTimeout = true - self.isOpaque = false - } - - override public init(layer: Any) { - preconditionFailure() - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func action(forKey event: String) -> CAAction? { - return nullAction - } - - func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable? { - if size != self.cellSize { - return nil - } - - let targetId = target.id - - if self.itemContexts[itemId] == nil { - for i in 0 ..< self.slotCount { - if self.slotToItemId[i] == nil { - self.slotToItemId[i] = itemId - self.itemContexts[itemId] = ItemContext(slotIndex: i, preferredRowAlignment: self.preferredRowAlignment, cache: cache, itemId: itemId, size: size, fetch: fetch, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - break - } - } - } - - if let itemContext = self.itemContexts[itemId] { - itemContext.targets.append(TargetReference(target)) - - let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in - Queue.mainQueue().async { - guard let strongSelf = self, let currentItemContext = strongSelf.itemContexts[itemId], currentItemContext === itemContext else { - return - } - strongSelf.removeTargetFromItemContext(itemId: itemId, itemContext: currentItemContext, targetId: targetId) - } - } - - let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in - guard let itemContext = itemContext else { - return - } - itemContext.updateIsPlaying() - } - - target.contents = self.contents - - let slotX = itemContext.slotIndex % self.slotsX - let slotY = itemContext.slotIndex / self.slotsX - let totalX = CGFloat(self.slotsX) * self.cellSize.width - let totalY = CGFloat(self.slotsY) * self.cellSize.height - let contentsRect = CGRect(origin: CGPoint(x: (CGFloat(slotX) * self.cellSize.width) / totalX, y: (CGFloat(slotY) * self.cellSize.height) / totalY), size: CGSize(width: self.cellSize.width / totalX, height: self.cellSize.height / totalY)) - target.contentsRect = contentsRect - - self.isPlaying = true - - return ActionDisposable { [weak self, weak target, weak itemContext] in - Queue.mainQueue().async { - guard let strongSelf = self, let currentItemContext = strongSelf.itemContexts[itemId], currentItemContext === itemContext else { - return - } - - if let target = target { - target.deinitCallbacks.remove(deinitIndex) - target.updateStateCallbacks.remove(updateStateIndex) - } - - strongSelf.removeTargetFromItemContext(itemId: itemId, itemContext: currentItemContext, targetId: targetId) - } - } - } else { - return nil - } - } - - private func removeTargetFromItemContext(itemId: String, itemContext: ItemContext, targetId: Int64) { - if let index = itemContext.targets.firstIndex(where: { $0.id == targetId }) { - itemContext.targets.remove(at: index) - - if itemContext.targets.isEmpty { - self.slotToItemId[itemContext.slotIndex] = nil - self.itemContexts.removeValue(forKey: itemId) - - if self.itemContexts.isEmpty { - self.isPlaying = false - } - } - } - } - - private func updateIsPlaying() { - var isPlaying = false - for (_, itemContext) in self.itemContexts { - if itemContext.isPlaying { - isPlaying = true - break - } - } - - self.isPlaying = isPlaying - } - - func animationTick(advanceTimestamp: Double) -> [LoadFrameTask] { - var tasks: [LoadFrameTask] = [] - for (_, itemContext) in self.itemContexts { - if itemContext.isPlaying { - if let task = itemContext.animationTick(device: self.metalDevice, texturePoolFullPlane: self.texturePoolFullPlane, texturePoolHalfPlane: self.texturePoolHalfPlane, advanceTimestamp: advanceTimestamp) { - tasks.append(task) - } - } - } - - return tasks - } - - func redraw() { - guard let drawable = self.nextDrawable() else { - return - } - - let commandQueue = self.commandQueue - let renderPipelineState = self.renderPipelineState - let cellSize = self.cellSize - - guard let commandBuffer = commandQueue.makeCommandBuffer() else { - return - } - - /*let drawTime = CACurrentMediaTime() - timestamp - if drawTime > 9.0 / 1000.0 { - print("get time \(drawTime * 1000.0)") - }*/ - - let renderPassDescriptor = MTLRenderPassDescriptor() - renderPassDescriptor.colorAttachments[0].texture = drawable.texture - renderPassDescriptor.colorAttachments[0].loadAction = .clear - renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor( - red: 0.0, - green: 0.0, - blue: 0.0, - alpha: 0.0 - ) - - guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { - return - } - - var usedTextures: [Unmanaged] = [] - - var vertices: [Float] = [ - -1.0, -1.0, 0.0, 0.0, - 1.0, -1.0, 1.0, 0.0, - -1.0, 1.0, 0.0, 1.0, - 1.0, 1.0, 1.0, 1.0 - ] - - renderEncoder.setRenderPipelineState(renderPipelineState) - - var resolution = simd_uint2(UInt32(drawable.texture.width), UInt32(drawable.texture.height)) - renderEncoder.setVertexBytes(&resolution, length: MemoryLayout.size * 2, index: 1) - - var slotSize = simd_uint2(UInt32(cellSize.width), UInt32(cellSize.height)) - renderEncoder.setVertexBytes(&slotSize, length: MemoryLayout.size * 2, index: 2) - - for (_, itemContext) in self.itemContexts { - guard let frame = itemContext.currentFrame else { - continue - } - - let slotX = itemContext.slotIndex % self.slotsX - let slotY = self.slotsY - 1 - itemContext.slotIndex / self.slotsY - let totalX = CGFloat(self.slotsX) * self.cellSize.width - let totalY = CGFloat(self.slotsY) * self.cellSize.height - - let contentsRect = CGRect(origin: CGPoint(x: (CGFloat(slotX) * self.cellSize.width) / totalX, y: (CGFloat(slotY) * self.cellSize.height) / totalY), size: CGSize(width: self.cellSize.width / totalX, height: self.cellSize.height / totalY)) - - vertices[4 * 2 + 0] = Float(contentsRect.minX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - vertices[4 * 2 + 1] = Float(contentsRect.minY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - - vertices[4 * 3 + 0] = Float(contentsRect.maxX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - vertices[4 * 3 + 1] = Float(contentsRect.minY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - - vertices[4 * 0 + 0] = Float(contentsRect.minX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - vertices[4 * 0 + 1] = Float(contentsRect.maxY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - - vertices[4 * 1 + 0] = Float(contentsRect.maxX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - vertices[4 * 1 + 1] = Float(contentsRect.maxY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) - - renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0) - - var slotPosition = simd_uint2(UInt32(itemContext.slotIndex % self.slotsX), UInt32(itemContext.slotIndex % self.slotsY)) - renderEncoder.setVertexBytes(&slotPosition, length: MemoryLayout.size * 2, index: 3) - - usedTextures.append(Unmanaged.passRetained(frame.textureY)) - usedTextures.append(Unmanaged.passRetained(frame.textureU)) - usedTextures.append(Unmanaged.passRetained(frame.textureV)) - usedTextures.append(Unmanaged.passRetained(frame.textureA)) - renderEncoder.setFragmentTexture(frame.textureY.content.texture, index: 0) - renderEncoder.setFragmentTexture(frame.textureU.content.texture, index: 1) - renderEncoder.setFragmentTexture(frame.textureV.content.texture, index: 2) - renderEncoder.setFragmentTexture(frame.textureA.content.texture, index: 3) - - renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1) - } - - renderEncoder.endEncoding() - - if self.presentsWithTransaction { - if Thread.isMainThread { - commandBuffer.commit() - commandBuffer.waitUntilScheduled() - drawable.present() - } else { - CATransaction.begin() - commandBuffer.commit() - commandBuffer.waitUntilScheduled() - drawable.present() - CATransaction.commit() - } - } else { - commandBuffer.addScheduledHandler { _ in - drawable.present() - } - commandBuffer.addCompletedHandler { _ in - DispatchQueue.main.async { - for texture in usedTextures { - texture.release() - } - } - } - commandBuffer.commit() - } - } - } - - private var nextSurfaceLayerIndex: Int = 1 - private var surfaceLayers: [Int: SurfaceLayer] = [:] - - private var frameSkip: Int - private var displayLink: ConstantDisplayLinkAnimator? - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - if self.isPlaying { - if self.displayLink == nil { - self.displayLink = ConstantDisplayLinkAnimator { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.animationTick() - } - self.displayLink?.frameInterval = self.frameSkip - self.displayLink?.isPaused = false - } - } else { - if let displayLink = self.displayLink { - self.displayLink = nil - displayLink.invalidate() - } - } - } - } - } - - public init() { - if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.processorCount > 2 { - self.frameSkip = 1 - } else { - self.frameSkip = 2 - } - } - - private func updateIsPlaying() { - var isPlaying = false - for (_, surfaceLayer) in self.surfaceLayers { - if surfaceLayer.isPlaying { - isPlaying = true - break - } - } - - self.isPlaying = isPlaying - } - - public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { - assert(Thread.isMainThread) - - let alignedSize = CGSize(width: CGFloat(alignUp(size: Int(size.width), align: 16)), height: CGFloat(alignUp(size: Int(size.height), align: 16))) - - for (_, surfaceLayer) in self.surfaceLayers { - if let disposable = surfaceLayer.add(target: target, cache: cache, itemId: itemId, unique: unique, size: alignedSize, fetch: fetch) { - return disposable - } - } - - let index = self.nextSurfaceLayerIndex - self.nextSurfaceLayerIndex += 1 - let surfaceLayer = SurfaceLayer(cellSize: alignedSize, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.surfaceLayers[index] = surfaceLayer - if let disposable = surfaceLayer.add(target: target, cache: cache, itemId: itemId, unique: unique, size: alignedSize, fetch: fetch) { - return disposable - } else { - return EmptyDisposable - } - } - - public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { - return false - } - - public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { - completion(false, true) - - return EmptyDisposable - } - - public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { - } - - private func animationTick() { - let secondsPerFrame = Double(self.frameSkip) / 60.0 - - var tasks: [LoadFrameTask] = [] - var surfaceLayersWithTasks: [Int] = [] - for (index, surfaceLayer) in self.surfaceLayers { - var hasTasks = false - if surfaceLayer.isPlaying { - let surfaceLayerTasks = surfaceLayer.animationTick(advanceTimestamp: secondsPerFrame) - if !surfaceLayerTasks.isEmpty { - tasks.append(contentsOf: surfaceLayerTasks) - hasTasks = true - } - } - if hasTasks { - surfaceLayersWithTasks.append(index) - } - } - - if !tasks.isEmpty { - ItemContext.queue.async { [weak self] in - var completions: [() -> Void] = [] - for task in tasks { - let complete = task.task() - completions.append(complete) - } - - if !completions.isEmpty { - Queue.mainQueue().async { - for completion in completions { - completion() - } - - if let strongSelf = self { - for index in surfaceLayersWithTasks { - if let surfaceLayer = strongSelf.surfaceLayers[index] { - surfaceLayer.redraw() - } - } - } - } - } - } - } - } -} diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index 6c2b72a6ed..bde9a11206 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -9,6 +9,7 @@ public protocol MultiAnimationRenderer: AnyObject { func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable + func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) } @@ -600,6 +601,34 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { }) } + func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { + return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { item in + guard let item = item.item else { + Queue.mainQueue().async { + completion(nil) + } + return + } + + let loadedFrame: ItemAnimationContext.Frame? + if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { + loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) + } else { + loadedFrame = nil + } + + Queue.mainQueue().async { + if let loadedFrame = loadedFrame { + if let cgImage = loadedFrame.image.cgImage { + completion(cgImage) + } + } else { + completion(nil) + } + } + }) + } + func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { if let itemContext = self.itemContexts[ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: 0)] { itemContext.setFrameIndex(index: frameIndex, placeholder: placeholder) @@ -737,6 +766,23 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion) } + public func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + return groupContext.loadFirstFrameAsImage(cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion) + } + public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { if let groupContext = self.groupContext { groupContext.setFrameIndex(itemId: itemId, size: size, frameIndex: frameIndex, placeholder: placeholder) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 38d97818b2..415c3dca0c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -92,6 +92,7 @@ swift_library( "//submodules/TelegramUI/Components/OptionButtonComponent", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/AnimatedCountLabelNode", + "//submodules/StickerResources", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift index 0f0db76c1e..d21946e61b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift @@ -18,36 +18,211 @@ import TextFormat import AnimatedCountLabelNode import LottieComponent import LottieComponentResourceContent +import StickerResources +import AnimationCache -public final class StaticStoryItemOverlaysView: UIImageView { - override public init(frame: CGRect) { - super.init(frame: frame) - } +private let shadowImage: UIImage = { + return UIImage(bundleImageName: "Stories/ReactionShadow")! +}() + +private let coverImage: UIImage = { + return UIImage(bundleImageName: "Stories/ReactionOutline")! +}() + +private let darkCoverImage: UIImage = { + return generateTintedImage(image: UIImage(bundleImageName: "Stories/ReactionOutline"), color: UIColor(rgb: 0x000000, alpha: 0.5))! +}() + +public func storyPreviewWithAddedReactions( + context: AccountContext, + storyItem: Stories.Item, + signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> +) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + var reactionData: [Signal<(MessageReaction.Reaction, CGImage?), NoError>] = [] - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - - } - - public func update( - context: AccountContext, - peer: EnginePeer, - story: EngineStoryItem, - availableReactions: StoryAvailableReactions?, - entityFiles: [MediaId: TelegramMediaFile] - ) { - - } - - override public func draw(_ rect: CGRect) { - guard let context = UIGraphicsGetCurrentContext() else { - return + let loadFile: (MessageReaction.Reaction, TelegramMediaFile) -> Signal<(MessageReaction.Reaction, CGImage?), NoError> = { reaction, file in + return Signal { subscriber in + subscriber.putNext((reaction, nil)) + + let isTemplate = !"".isEmpty + return context.animationRenderer.loadFirstFrameAsImage(cache: context.animationCache, itemId: file.resource.id.stringRepresentation, size: CGSize(width: 128.0, height: 128.0), fetch: animationCacheFetchFile(postbox: context.account.postbox, userLocation: .other, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { result in + subscriber.putNext((reaction, result)) + if result != nil { + subscriber.putCompletion() + } + }) } + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs.0 != rhs.0 { + return false + } + if lhs.1 !== rhs.1 { + return false + } + return true + }) + } + + var availableReactions: Promise? + var processedReactions: [MessageReaction.Reaction] = [] + var customFileIds: [Int64] = [] + for mediaArea in storyItem.mediaAreas { + if case let .reaction(_, reaction, _) = mediaArea { + if processedReactions.contains(reaction) { + continue + } + processedReactions.append(reaction) + + switch reaction { + case .builtin: + if availableReactions == nil { + availableReactions = Promise() + availableReactions?.set(context.engine.stickers.availableReactions()) + } + reactionData.append(availableReactions!.get() + |> take(1) + |> mapToSignal { availableReactions -> Signal<(MessageReaction.Reaction, CGImage?), NoError> in + guard let availableReactions else { + return .single((reaction, nil)) + } + for item in availableReactions.reactions { + if item.value == reaction { + guard let file = item.centerAnimation else { + break + } + return loadFile(reaction, file) + } + } + return .single((reaction, nil)) + }) + case let .custom(fileId): + if !customFileIds.contains(fileId) { + customFileIds.append(fileId) + } + } + } + } + + if !customFileIds.isEmpty { + let customFiles = Promise<[Int64: TelegramMediaFile]>() + customFiles.set(context.engine.stickers.resolveInlineStickers(fileIds: customFileIds)) - let _ = context + for id in customFileIds { + reactionData.append(customFiles.get() + |> take(1) + |> mapToSignal { customFiles -> Signal<(MessageReaction.Reaction, CGImage?), NoError> in + let reaction: MessageReaction.Reaction = .custom(id) + + guard let file = customFiles[id] else { + return .single((reaction, nil)) + } + + return loadFile(reaction, file) + }) + } + } + + return combineLatest( + signal, + combineLatest(reactionData) + ) + |> map { draw, reactionsData in + return { arguments in + guard let context = draw(arguments) else { + return nil + } + + let drawingRect = arguments.drawingRect + var fittedSize = arguments.imageSize + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height + } + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + context.withContext { c in + c.concatenate(c.ctm.inverted()) + c.scaleBy(x: context.scale, y: context.scale) + } + + context.withFlippedContext { c in + c.setBlendMode(.normal) + + for mediaArea in storyItem.mediaAreas { + c.saveGState() + defer { + c.restoreGState() + } + + if case let .reaction(coordinates, reaction, flags) = mediaArea { + let _ = reaction + let _ = flags + + let referenceSize = fittedRect.size + var areaSize = CGSize(width: coordinates.width / 100.0 * referenceSize.width, height: coordinates.height / 100.0 * referenceSize.height) + areaSize.width *= 0.97 + areaSize.height *= 0.97 + let targetFrame = CGRect(x: coordinates.x / 100.0 * referenceSize.width - areaSize.width * 0.5, y: coordinates.y / 100.0 * referenceSize.height - areaSize.height * 0.5, width: areaSize.width, height: areaSize.height) + if targetFrame.width < 2.0 || targetFrame.height < 2.0 { + continue + } + + c.saveGState() + + c.translateBy(x: targetFrame.midX, y: targetFrame.midY) + c.scaleBy(x: flags.contains(.isFlipped) ? -1.0 : 1.0, y: -1.0) + c.rotate(by: -coordinates.rotation * (CGFloat.pi / 180.0)) + c.translateBy(x: -targetFrame.midX, y: -targetFrame.midY) + + let insets = UIEdgeInsets(top: -0.08, left: -0.05, bottom: -0.01, right: -0.02) + let coverFrame = CGRect(origin: CGPoint(x: targetFrame.width * insets.left, y: targetFrame.height * insets.top), size: CGSize(width: targetFrame.width - targetFrame.width * insets.left - targetFrame.width * insets.right, height: targetFrame.height - targetFrame.height * insets.top - targetFrame.height * insets.bottom)).offsetBy(dx: targetFrame.minX, dy: targetFrame.minY) + + c.draw(shadowImage.cgImage!, in: coverFrame) + + if flags.contains(.isDark) { + c.draw(darkCoverImage.cgImage!, in: coverFrame) + } else { + c.draw(coverImage.cgImage!, in: coverFrame) + } + + c.restoreGState() + + c.translateBy(x: targetFrame.midX, y: targetFrame.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.rotate(by: -coordinates.rotation * (CGFloat.pi / 180.0)) + c.translateBy(x: -targetFrame.midX, y: -targetFrame.midY) + + let minSide = floor(min(200.0, min(targetFrame.width, targetFrame.height)) * 0.5) + let itemSize = CGSize(width: minSide, height: minSide) + + if let (_, maybeImage) = reactionsData.first(where: { $0.0 == reaction }), let image = maybeImage { + var imageFrame = itemSize.centered(around: targetFrame.center.offsetBy(dx: 0.0, dy: -targetFrame.height * 0.05)) + if case .builtin = reaction { + imageFrame = imageFrame.insetBy(dx: -imageFrame.width * 0.5, dy: -imageFrame.height * 0.5) + } + + c.draw(image, in: imageFrame) + } + } + } + } + + context.withContext { c in + c.concatenate(c.ctm.inverted()) + c.scaleBy(x: context.scale, y: context.scale) + + c.scaleBy(x: context.size.width * 0.5, y: context.size.height * 0.5) + c.scaleBy(x: 1.0, y: -1.0) + c.scaleBy(x: -context.size.width * 0.5, y: -context.size.height * 0.5) + } + + addCorners(context, arguments: arguments) + + return context + } } } @@ -56,14 +231,6 @@ final class StoryItemOverlaysView: UIView { return Font.with(size: 17.0, design: .camera, weight: .semibold, traits: .monospacedNumbers) }() - private static let shadowImage: UIImage = { - return UIImage(bundleImageName: "Stories/ReactionShadow")! - }() - - private static let coverImage: UIImage = { - return UIImage(bundleImageName: "Stories/ReactionOutline")! - }() - private final class ItemView: HighlightTrackingButton { private let shadowView: UIImageView private let coverView: UIImageView @@ -83,8 +250,8 @@ final class StoryItemOverlaysView: UIView { private var customEmojiLoadDisposable: Disposable? override init(frame: CGRect) { - self.shadowView = UIImageView(image: StoryItemOverlaysView.shadowImage) - self.coverView = UIImageView(image: StoryItemOverlaysView.coverImage) + self.shadowView = UIImageView(image: shadowImage) + self.coverView = UIImageView(image: coverImage) super.init(frame: frame) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 6dfa7bd625..0a3f97c187 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -24,6 +24,7 @@ import ChatMessageInteractiveMediaBadge import ContextUI import InvisibleInkDustNode import ChatControllerInteraction +import StoryContainerScreen private struct FetchControls { let fetch: (Bool) -> Void @@ -940,7 +941,16 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if !mediaUpdated, let media = media as? TelegramMediaStory { if message.associatedStories[media.storyId] != currentMessage?.associatedStories[media.storyId] { - mediaUpdated = true + let previousStory = message.associatedStories[media.storyId] + let updatedStory = currentMessage?.associatedStories[media.storyId] + + if let previousItem = previousStory?.get(Stories.StoredItem.self), let updatedItem = updatedStory?.get(Stories.StoredItem.self), case let .item(previousItemValue) = previousItem, case let .item(updatedItemValue) = updatedItem { + if let previousItemMedia = previousItemValue.media, let updatedItemMedia = updatedItemValue.media { + mediaUpdated = !previousItemMedia.isSemanticallyEqual(to: updatedItemMedia) + } + } else { + mediaUpdated = true + } } } } else { @@ -1037,7 +1047,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } else { updateImageSignal = { synchronousLoad, highQuality in - return chatMessagePhoto(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad, highQuality: highQuality) + return storyPreviewWithAddedReactions(context: context, storyItem: item, signal: chatMessagePhoto(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad, highQuality: highQuality)) } updateBlurredImageSignal = { synchronousLoad, _ in return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), ignoreFullSize: true, synchronousLoad: true) @@ -1074,7 +1084,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else { onlyFullSizeVideoThumbnail = isSendingUpdated updateImageSignal = { synchronousLoad, _ in - return mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true) + return storyPreviewWithAddedReactions(context: context, storyItem: item, signal: mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true)) } updateBlurredImageSignal = { synchronousLoad, _ in return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: true)