Swiftgram/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift
2023-09-21 00:12:53 +02:00

322 lines
15 KiB
Swift
Executable File

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)
}
}
}
}