import Foundation import UIKit import SwiftSignalKit import Display import AnimationCache import MultiAnimationRenderer import ComponentFlow import AccountContext import TelegramCore import Postbox import EmojiTextAttachmentView import AppBundle import TextFormat import Lottie import GZip import HierarchyTrackingLayer public final class EmojiStatusComponent: Component { public typealias EnvironmentType = Empty public enum AnimationContent: Equatable { case file(file: TelegramMediaFile) case customEmoji(fileId: Int64) var fileId: MediaId { switch self { case let .file(file): return file.fileId case let .customEmoji(fileId): return MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) } } } public enum LoopMode: Equatable { case forever case count(Int) } public enum Content: Equatable { case none case premium(color: UIColor) case verified(fillColor: UIColor, foregroundColor: UIColor) case fake(color: UIColor) case scam(color: UIColor) case animation(content: AnimationContent, size: CGSize, placeholderColor: UIColor, themeColor: UIColor?, loopMode: LoopMode) } public let context: AccountContext public let animationCache: AnimationCache public let animationRenderer: MultiAnimationRenderer public let content: Content public let isVisibleForAnimations: Bool public let action: (() -> Void)? public let emojiFileUpdated: ((TelegramMediaFile?) -> Void)? public init( context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, content: Content, isVisibleForAnimations: Bool, action: (() -> Void)?, emojiFileUpdated: ((TelegramMediaFile?) -> Void)? = nil ) { self.context = context self.animationCache = animationCache self.animationRenderer = animationRenderer self.content = content self.isVisibleForAnimations = isVisibleForAnimations self.action = action self.emojiFileUpdated = emojiFileUpdated } public func withVisibleForAnimations(_ isVisibleForAnimations: Bool) -> EmojiStatusComponent { return EmojiStatusComponent( context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, content: self.content, isVisibleForAnimations: isVisibleForAnimations, action: self.action, emojiFileUpdated: self.emojiFileUpdated ) } public static func ==(lhs: EmojiStatusComponent, rhs: EmojiStatusComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.animationCache !== rhs.animationCache { return false } if lhs.animationRenderer !== rhs.animationRenderer { return false } if lhs.content != rhs.content { return false } if lhs.isVisibleForAnimations != rhs.isVisibleForAnimations { return false } return true } public final class View: UIView { private final class AnimationFileProperties { let path: String let coloredComposition: Animation? init(path: String, coloredComposition: Animation?) { self.path = path self.coloredComposition = coloredComposition } static func load(from path: String) -> AnimationFileProperties { guard let size = fileSize(path), size < 1024 * 1024 else { return AnimationFileProperties(path: path, coloredComposition: nil) } guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return AnimationFileProperties(path: path, coloredComposition: nil) } guard let unzippedData = TGGUnzipData(data, 1024 * 1024) else { return AnimationFileProperties(path: path, coloredComposition: nil) } var coloredComposition: Animation? if let composition = try? Animation.from(data: unzippedData) { coloredComposition = composition } return AnimationFileProperties(path: path, coloredComposition: coloredComposition) } } private weak var state: EmptyComponentState? private var component: EmojiStatusComponent? private var iconView: UIImageView? private var animationLayer: InlineStickerItemLayer? private var lottieAnimationView: AnimationView? private let hierarchyTrackingLayer: HierarchyTrackingLayer private var emojiFile: TelegramMediaFile? private var emojiFileDataProperties: AnimationFileProperties? private var emojiFileDisposable: Disposable? private var emojiFileDataPathDisposable: Disposable? override init(frame: CGRect) { self.hierarchyTrackingLayer = HierarchyTrackingLayer() super.init(frame: frame) self.layer.addSublayer(self.hierarchyTrackingLayer) self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in guard let strongSelf = self else { return } if let lottieAnimationView = strongSelf.lottieAnimationView { lottieAnimationView.play() } } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.emojiFileDisposable?.dispose() self.emojiFileDataPathDisposable?.dispose() } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.component?.action?() } } func update(component: EmojiStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.state = state var iconImage: UIImage? var emojiFileId: Int64? var emojiPlaceholderColor: UIColor? var emojiThemeColor: UIColor? var emojiLoopMode: LoopMode? var emojiSize = CGSize() self.isUserInteractionEnabled = component.action != nil //let previousContent = self.component?.content if self.component?.content != component.content { switch component.content { case .none: iconImage = nil case let .premium(color): if let sourceImage = UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon") { iconImage = generateImage(sourceImage.size, contextGenerator: { size, context in if let cgImage = sourceImage.cgImage { context.clear(CGRect(origin: CGPoint(), size: size)) let imageSize = CGSize(width: sourceImage.size.width - 8.0, height: sourceImage.size.height - 8.0) context.clip(to: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize), mask: cgImage) context.setFillColor(color.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) } }, opaque: false) } else { iconImage = nil } case let .verified(fillColor, foregroundColor): if let backgroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconForeground") { iconImage = generateImage(backgroundImage.size, contextGenerator: { size, context in if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { context.clear(CGRect(origin: CGPoint(), size: size)) context.saveGState() context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage) context.setFillColor(fillColor.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) context.restoreGState() context.setBlendMode(.copy) context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) context.setFillColor(foregroundColor.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) } }, opaque: false) } else { iconImage = nil } case .fake: iconImage = nil case .scam: iconImage = nil case let .animation(animationContent, size, placeholderColor, themeColor, loopMode): iconImage = nil emojiFileId = animationContent.fileId.id emojiPlaceholderColor = placeholderColor emojiThemeColor = themeColor emojiSize = size emojiLoopMode = loopMode if case let .animation(previousAnimationContent, _, _, _, _) = self.component?.content { if previousAnimationContent.fileId != animationContent.fileId { self.emojiFileDisposable?.dispose() self.emojiFileDisposable = nil self.emojiFileDataPathDisposable?.dispose() self.emojiFileDataPathDisposable = nil self.emojiFile = nil self.emojiFileDataProperties = nil if let animationLayer = self.animationLayer { self.animationLayer = nil if !transition.animation.isImmediate { animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak animationLayer] _ in animationLayer?.removeFromSuperlayer() }) animationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } else { animationLayer.removeFromSuperlayer() } } if let lottieAnimationView = self.lottieAnimationView { self.lottieAnimationView = nil if !transition.animation.isImmediate { lottieAnimationView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak lottieAnimationView] _ in lottieAnimationView?.removeFromSuperview() }) lottieAnimationView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } else { lottieAnimationView.removeFromSuperview() } } } } switch animationContent { case let .file(file): self.emojiFile = file case .customEmoji: break } } } else { iconImage = self.iconView?.image if case let .animation(animationContent, size, placeholderColor, themeColor, loopMode) = component.content { emojiFileId = animationContent.fileId.id emojiPlaceholderColor = placeholderColor emojiThemeColor = themeColor emojiLoopMode = loopMode emojiSize = size } } self.component = component var size = CGSize() if let iconImage = iconImage { let iconView: UIImageView if let current = self.iconView { iconView = current } else { iconView = UIImageView() self.iconView = iconView self.addSubview(iconView) if !transition.animation.isImmediate { iconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) iconView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) } } iconView.image = iconImage size = iconImage.size.aspectFilled(availableSize) iconView.frame = CGRect(origin: CGPoint(), size: size) } else { if let iconView = self.iconView { self.iconView = nil if !transition.animation.isImmediate { iconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak iconView] _ in iconView?.removeFromSuperview() }) iconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } else { iconView.removeFromSuperview() } } } let emojiFileUpdated = component.emojiFileUpdated if let emojiFileId = emojiFileId, let emojiPlaceholderColor = emojiPlaceholderColor, let emojiLoopMode = emojiLoopMode { size = availableSize let _ = emojiLoopMode if let emojiFile = self.emojiFile { self.emojiFileDisposable?.dispose() self.emojiFileDisposable = nil self.emojiFileDataPathDisposable?.dispose() self.emojiFileDataPathDisposable = nil /*if !"".isEmpty { var resetThemeColor = false let lottieAnimationView: AnimationView if let current = self.lottieAnimationView { lottieAnimationView = current if case let .animation(_, _, _, previousThemeColor, _) = previousContent { if previousThemeColor != emojiThemeColor { resetThemeColor = true } } else { resetThemeColor = true } } else { resetThemeColor = true lottieAnimationView = AnimationView(animation: coloredComposition) lottieAnimationView.loopMode = .loop self.lottieAnimationView = lottieAnimationView self.addSubview(lottieAnimationView) if !transition.animation.isImmediate { lottieAnimationView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) lottieAnimationView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) } } if resetThemeColor { for keypath in lottieAnimationView.allKeypaths(predicate: { $0.keys.contains(where: { $0.contains("_theme") }) && $0.keys.last == "Color" }) { lottieAnimationView.setValueProvider(ColorValueProvider(emojiThemeColor.lottieColorValue), keypath: AnimationKeypath(keypath: keypath)) } } lottieAnimationView.frame = CGRect(origin: CGPoint(), size: size) if component.isVisibleForAnimations { if !lottieAnimationView.isAnimationPlaying { lottieAnimationView.play() } } else { if lottieAnimationView.isAnimationPlaying { lottieAnimationView.stop() } } } else {*/ let animationLayer: InlineStickerItemLayer if let current = self.animationLayer { animationLayer = current } else { let loopCount: Int? switch emojiLoopMode { case .forever: loopCount = nil case let .count(value): loopCount = value } animationLayer = InlineStickerItemLayer( context: component.context, attemptSynchronousLoad: false, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiFile.fileId.id, file: emojiFile), file: emojiFile, cache: component.animationCache, renderer: component.animationRenderer, unique: true, placeholderColor: emojiPlaceholderColor, pointSize: emojiSize, loopCount: loopCount ) self.animationLayer = animationLayer self.layer.addSublayer(animationLayer) if !transition.animation.isImmediate { animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) animationLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) } } var accentTint = false if let _ = emojiThemeColor { for attribute in emojiFile.attributes { if case let .CustomEmoji(_, _, packReference) = attribute { switch packReference { case let .id(id, _): if id == 773947703670341676 { accentTint = true } default: break } } } } if accentTint { animationLayer.contentTintColor = emojiThemeColor } else { animationLayer.contentTintColor = nil } animationLayer.frame = CGRect(origin: CGPoint(), size: size) animationLayer.isVisibleForAnimations = component.isVisibleForAnimations /*} else { if self.emojiFileDataPathDisposable == nil { let account = component.context.account self.emojiFileDataPathDisposable = (Signal { subscriber in let disposable = MetaDisposable() let _ = (account.postbox.mediaBox.resourceData(emojiFile.resource) |> take(1)).start(next: { firstAttemptData in if firstAttemptData.complete { subscriber.putNext(AnimationFileProperties.load(from: firstAttemptData.path)) subscriber.putCompletion() } else { let fetchDisposable = freeMediaFileInteractiveFetched(account: account, fileReference: .standalone(media: emojiFile)).start() let dataDisposable = account.postbox.mediaBox.resourceData(emojiFile.resource).start(next: { data in if data.complete { subscriber.putNext(AnimationFileProperties.load(from: data.path)) subscriber.putCompletion() } }) disposable.set(ActionDisposable { fetchDisposable.dispose() dataDisposable.dispose() }) } }) return disposable } |> deliverOnMainQueue).start(next: { [weak self] properties in guard let strongSelf = self else { return } strongSelf.emojiFileDataProperties = properties strongSelf.state?.updated(transition: transition) }) } }*/ } else { if self.emojiFileDisposable == nil { self.emojiFileDisposable = (component.context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId]) |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return } strongSelf.emojiFile = result[emojiFileId] strongSelf.emojiFileDataProperties = nil strongSelf.state?.updated(transition: transition) emojiFileUpdated?(result[emojiFileId]) }) } } } else { if let _ = self.emojiFile { self.emojiFile = nil self.emojiFileDataProperties = nil emojiFileUpdated?(nil) } self.emojiFileDisposable?.dispose() self.emojiFileDisposable = nil self.emojiFileDataPathDisposable?.dispose() self.emojiFileDataPathDisposable = nil if let animationLayer = self.animationLayer { self.animationLayer = nil if !transition.animation.isImmediate { animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak animationLayer] _ in animationLayer?.removeFromSuperlayer() }) animationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } else { animationLayer.removeFromSuperlayer() } } if let lottieAnimationView = self.lottieAnimationView { self.lottieAnimationView = nil if !transition.animation.isImmediate { lottieAnimationView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak lottieAnimationView] _ in lottieAnimationView?.removeFromSuperview() }) lottieAnimationView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } else { lottieAnimationView.removeFromSuperview() } } } return size } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }