import Foundation import UIKit import Display import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import ViewControllerComponent import ComponentDisplayAdapters import TelegramPresentationData import AccountContext import TelegramCore import MultilineTextComponent import EmojiStatusComponent import Postbox import AnimatedStickerNode import TelegramAnimatedStickerNode import StickerResources import AvatarBackground final class AvatarPreviewComponent: Component { typealias EnvironmentType = Empty let context: AccountContext let background: AvatarBackground let file: TelegramMediaFile? let tapped: () -> Void init( context: AccountContext, background: AvatarBackground, file: TelegramMediaFile?, tapped: @escaping () -> Void ) { self.context = context self.background = background self.file = file self.tapped = tapped } static func ==(lhs: AvatarPreviewComponent, rhs: AvatarPreviewComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.background != rhs.background { return false } if lhs.file != rhs.file { return false } return true } final class View: UIView, UITextFieldDelegate { private let imageView: UIImageView private let imageNode: TransformImageNode private var animationNode: AnimatedStickerNode? private var component: AvatarPreviewComponent? private weak var state: EmptyComponentState? private let stickerFetchedDisposable = MetaDisposable() private let cachedDisposable = MetaDisposable() override init(frame: CGRect) { self.imageView = UIImageView() self.imageView.isUserInteractionEnabled = false self.imageNode = TransformImageNode() super.init(frame: frame) self.disablesInteractiveModalDismiss = true self.disablesInteractiveKeyboardGestureRecognizer = true self.addSubview(self.imageView) self.addSubnode(self.imageNode) self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.stickerFetchedDisposable.dispose() self.cachedDisposable.dispose() } @objc func tapped() { self.animationNode?.playOnce() self.component?.tapped() } func update(component: AvatarPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousBackground = self.component?.background let hadFile = self.component?.file != nil var fileUpdated = false if self.component?.file?.fileId != component.file?.fileId { fileUpdated = true } self.component = component self.state = state let size = CGSize(width: availableSize.width * 0.66, height: availableSize.width * 0.66) var dimensions: CGSize? if let file = component.file, fileUpdated, let fileDimensions = file.dimensions?.cgSize { dimensions = fileDimensions if !self.imageNode.isHidden && hadFile, let snapshotView = self.imageNode.view.snapshotContentTree() { self.imageNode.view.superview?.addSubview(snapshotView) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) snapshotView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) } if let animationNode = self.animationNode { self.animationNode = nil animationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) animationNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { [weak animationNode] _ in animationNode?.removeFromSupernode() }) } self.imageNode.isHidden = false if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" { if self.animationNode == nil { let animationNode = DefaultAnimatedStickerNodeImpl() animationNode.autoplay = false self.animationNode = animationNode animationNode.started = { [weak self] in self?.imageNode.isHidden = true } self.addSubnode(animationNode) } self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: component.context.account.postbox, userLocation: .other, file: file, small: false, size: fileDimensions.aspectFitted(CGSize(width: 256.0, height: 256.0)))) self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start()) } else { if let animationNode = self.animationNode { animationNode.visibility = false self.animationNode = nil animationNode.removeFromSupernode() } self.imageNode.setSignal(chatMessageSticker(account: component.context.account, userLocation: .other, file: file, small: false, synchronousLoad: false)) self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: false)).start()) } if fileUpdated && hadFile { self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.imageNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) if let animationNode = self.animationNode { animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) animationNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) } } } if let dimensions { let imageSize = dimensions.aspectFitted(size) self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() self.imageNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - imageSize.width) / 2.0), y: (availableSize.height - imageSize.height) / 2.0), size: imageSize) if let animationNode = self.animationNode { animationNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - imageSize.width) / 2.0), y: (availableSize.height - imageSize.height) / 2.0), size: imageSize) animationNode.updateLayout(size: imageSize) } if fileUpdated { self.updateVisibility() } } self.imageView.frame = CGRect(origin: .zero, size: availableSize) if previousBackground != component.background { if let _ = previousBackground, !transition.animation.isImmediate { UIView.transition(with: self.imageView, duration: 0.2, options: .transitionCrossDissolve, animations: { self.imageView.image = component.background.generateImage(size: availableSize) }) } else { self.imageView.image = component.background.generateImage(size: availableSize) } self.imageView.image = component.background.generateImage(size: availableSize) } return availableSize } private func updateVisibility() { guard let component = self.component, let file = component.file else { return } let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) let source = AnimatedStickerResourceSource(account: component.context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm") self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .count(1), mode: .direct(cachePathPrefix: nil)) self.animationNode?.visibility = true if let animationNode = self.animationNode as? DefaultAnimatedStickerNodeImpl { if file.isCustomTemplateEmoji { animationNode.dynamicColor = .white } else { animationNode.dynamicColor = nil } } self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384) |> deliverOn(Queue.concurrentDefaultQueue())).start()) } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }