import Foundation import UIKit import Display import TelegramCore import SwiftSignalKit import AsyncDisplayKit import Postbox import StickerResources import AccountContext import AnimatedStickerNode import TelegramAnimatedStickerNode import ShimmerEffect import TelegramPresentationData import AccountContext final class HorizontalStickerGridItem: GridItem { let context: AccountContext let file: TelegramMediaFile let theme: PresentationTheme let isPreviewed: (HorizontalStickerGridItem) -> Bool let sendSticker: (FileMediaReference, UIView, CGRect) -> Void let section: GridSection? = nil init(context: AccountContext, file: TelegramMediaFile, theme: PresentationTheme, isPreviewed: @escaping (HorizontalStickerGridItem) -> Bool, sendSticker: @escaping (FileMediaReference, UIView, CGRect) -> Void) { self.context = context self.file = file self.theme = theme self.isPreviewed = isPreviewed self.sendSticker = sendSticker } func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { let node = HorizontalStickerGridItemNode() node.setup(context: self.context, item: self) node.sendSticker = self.sendSticker return node } func update(node: GridItemNode) { guard let node = node as? HorizontalStickerGridItemNode else { assertionFailure() return } node.setup(context: self.context, item: self) node.sendSticker = self.sendSticker } } final class HorizontalStickerGridItemNode: GridItemNode { private var currentState: (AccountContext, HorizontalStickerGridItem, CGSize)? let imageNode: TransformImageNode private(set) var animationNode: AnimatedStickerNode? private(set) var placeholderNode: StickerShimmerEffectNode? private var lockBackground: UIVisualEffectView? private var lockTintView: UIView? private var lockIconNode: ASImageNode? private let stickerFetchedDisposable = MetaDisposable() var sendSticker: ((FileMediaReference, UIView, CGRect) -> Void)? private var currentIsPreviewing: Bool = false private var setupTimestamp: Double? override var isVisibleInGrid: Bool { didSet { if oldValue != self.isVisibleInGrid { if self.isVisibleInGrid { if self.setupTimestamp == nil { self.setupTimestamp = CACurrentMediaTime() } self.animationNode?.visibility = true } else { self.animationNode?.visibility = false } } } } var stickerItem: StickerPackItem? { if let (_, item, _) = self.currentState { return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: item.file, indexKeys: []) } else { return nil } } override init() { self.imageNode = TransformImageNode() self.placeholderNode = StickerShimmerEffectNode() super.init() self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) self.addSubnode(self.imageNode) if let placeholderNode = self.placeholderNode { placeholderNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) self.addSubnode(placeholderNode) } var firstTime = true self.imageNode.imageUpdated = { [weak self] image in guard let strongSelf = self else { return } if image != nil { strongSelf.removePlaceholder(animated: !firstTime) } firstTime = false } } deinit { self.stickerFetchedDisposable.dispose() } private func removePlaceholder(animated: Bool) { if let placeholderNode = self.placeholderNode { self.placeholderNode = nil if !animated { placeholderNode.removeFromSupernode() } else { placeholderNode.allowsGroupOpacity = true placeholderNode.alpha = 0.0 placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in placeholderNode?.removeFromSupernode() placeholderNode?.allowsGroupOpacity = false }) } } } override func didLoad() { super.didLoad() self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) } func setup(context: AccountContext, item: HorizontalStickerGridItem) { if self.currentState == nil || self.currentState!.0 !== context || self.currentState!.1.file.id != item.file.id { if let dimensions = item.file.dimensions { if item.file.isAnimatedSticker || item.file.isVideoSticker { let animationNode: AnimatedStickerNode if let currentAnimationNode = self.animationNode { animationNode = currentAnimationNode } else { animationNode = DefaultAnimatedStickerNodeImpl() animationNode.transform = self.imageNode.transform animationNode.visibility = self.isVisibleInGrid animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) if let placeholderNode = self.placeholderNode { self.insertSubnode(animationNode, belowSubnode: placeholderNode) } else { self.addSubnode(animationNode) } self.animationNode = animationNode } let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) if item.file.isVideoSticker { self.imageNode.setSignal(chatMessageSticker(postbox: context.account.postbox, userLocation: .other, file: item.file, small: true, synchronousLoad: false)) } else { self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .other, file: item.file, small: true, size: fittedDimensions, synchronousLoad: false)) } animationNode.started = { [weak self] in guard let strongSelf = self else { return } strongSelf.imageNode.alpha = 0.0 let current = CACurrentMediaTime() if let setupTimestamp = strongSelf.setupTimestamp, current - setupTimestamp > 0.3 { if let placeholderNode = strongSelf.placeholderNode, !placeholderNode.alpha.isZero { strongSelf.removePlaceholder(animated: true) } } else { strongSelf.removePlaceholder(animated: false) } } animationNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: item.file.resource, isVideo: item.file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .cached) self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).startStrict()) } else { self.imageNode.alpha = 1.0 self.imageNode.setSignal(chatMessageSticker(account: context.account, userLocation: .other, file: item.file, small: true)) if let currentAnimationNode = self.animationNode { self.animationNode = nil currentAnimationNode.removeFromSupernode() } self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: stickerPackFileReference(item.file), resource: chatMessageStickerResource(file: item.file, small: true)).startStrict()) } if item.file.isPremiumSticker { let lockBackground: UIVisualEffectView let lockIconNode: ASImageNode if let currentBackground = self.lockBackground, let currentIcon = self.lockIconNode { lockBackground = currentBackground lockIconNode = currentIcon } else { let effect: UIBlurEffect if #available(iOS 10.0, *) { effect = UIBlurEffect(style: .regular) } else { effect = UIBlurEffect(style: .light) } lockBackground = UIVisualEffectView(effect: effect) lockBackground.clipsToBounds = true lockBackground.isUserInteractionEnabled = false lockIconNode = ASImageNode() lockIconNode.displaysAsynchronously = false lockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white) lockIconNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) let lockTintView = UIView() lockTintView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.15) lockBackground.contentView.addSubview(lockTintView) self.lockBackground = lockBackground self.lockTintView = lockTintView self.lockIconNode = lockIconNode self.view.addSubview(lockBackground) self.addSubnode(lockIconNode) } } else if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode { self.lockBackground = nil self.lockTintView = nil self.lockIconNode = nil lockBackground.removeFromSuperview() lockTintView.removeFromSuperview() lockIconNode.removeFromSupernode() } self.currentState = (context, item, dimensions.cgSize) self.setNeedsLayout() } } self.updatePreviewing(animated: false) } override func layout() { super.layout() let bounds = self.bounds let boundingSize = bounds.insetBy(dx: 2.0, dy: 2.0).size if let placeholderNode = self.placeholderNode { placeholderNode.frame = bounds if let context = self.currentState?.0, let theme = self.currentState?.1.theme, let file = self.currentState?.1.file { placeholderNode.update(backgroundColor: theme.list.plainBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor.mixedWith(theme.list.plainBackgroundColor, alpha: 0.4), shimmeringColor: theme.list.mediaPlaceholderColor.withAlphaComponent(0.3), data: file.immediateThumbnailData, size: bounds.size, enableEffect: context.sharedContext.energyUsageSettings.fullTranslucency) } } if let (_, _, mediaDimensions) = self.currentState { let imageSize = mediaDimensions.aspectFitted(boundingSize) self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() let imageFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: CGSize(width: imageSize.width, height: imageSize.height)) self.imageNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: imageSize.width, height: imageSize.height)) self.imageNode.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) if let animationNode = self.animationNode { animationNode.bounds = self.imageNode.bounds animationNode.position = self.imageNode.position animationNode.updateLayout(size: self.imageNode.bounds.size) } } if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode { let lockSize = CGSize(width: 16.0, height: 16.0) let lockBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: bounds.height - lockSize.height), size: lockSize) lockBackground.frame = lockBackgroundFrame lockBackground.layer.cornerRadius = lockSize.width / 2.0 if #available(iOS 13.0, *) { lockBackground.layer.cornerCurve = .circular } lockTintView.frame = CGRect(origin: CGPoint(), size: lockBackgroundFrame.size) if let icon = lockIconNode.image { let iconSize = CGSize(width: icon.size.width - 4.0, height: icon.size.height - 4.0) lockIconNode.frame = CGRect(origin: CGPoint(x: lockBackgroundFrame.minX + floorToScreenPixels((lockBackgroundFrame.width - iconSize.width) / 2.0), y: lockBackgroundFrame.minY + floorToScreenPixels((lockBackgroundFrame.height - iconSize.height) / 2.0)), size: iconSize) } } } override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { if let placeholderNode = self.placeholderNode { placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize) } } @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { if let (_, item, _) = self.currentState, case .ended = recognizer.state { self.sendSticker?(.standalone(media: item.file), self.view, self.bounds) } } func transitionNode() -> ASDisplayNode? { return self.imageNode } func updatePreviewing(animated: Bool) { let isPreviewing = false if self.currentIsPreviewing != isPreviewing { self.currentIsPreviewing = isPreviewing self.layer.sublayerTransform = CATransform3DIdentity if animated { self.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.5) } } } }