mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
246 lines
11 KiB
Swift
246 lines
11 KiB
Swift
import Foundation
|
||
import UIKit
|
||
import AsyncDisplayKit
|
||
import Display
|
||
import SwiftSignalKit
|
||
import Postbox
|
||
import TelegramCore
|
||
import AccountContext
|
||
import ConfettiEffect
|
||
import AnimatedStickerNode
|
||
import TelegramAnimatedStickerNode
|
||
import TelegramStringFormatting
|
||
|
||
final class PeerInfoBirthdayOverlay: ASDisplayNode {
|
||
private let context: AccountContext
|
||
|
||
private var disposable: Disposable?
|
||
|
||
init(context: AccountContext) {
|
||
self.context = context
|
||
|
||
super.init()
|
||
|
||
self.isUserInteractionEnabled = false
|
||
}
|
||
|
||
deinit {
|
||
self.disposable?.dispose()
|
||
}
|
||
|
||
func setup(size: CGSize, birthday: TelegramBirthday, sourceRect: CGRect?) {
|
||
self.setupAnimations(size: size, birthday: birthday, sourceRect: sourceRect)
|
||
|
||
Queue.mainQueue().after(0.1) {
|
||
self.view.addSubview(ConfettiView(frame: CGRect(origin: .zero, size: size)))
|
||
}
|
||
}
|
||
|
||
private func setupAnimations(size: CGSize, birthday: TelegramBirthday, sourceRect: CGRect?) {
|
||
self.disposable = (combineLatest(
|
||
self.context.engine.stickers.loadedStickerPack(reference: .animatedEmojiAnimations, forceActualized: false),
|
||
self.context.engine.stickers.loadedStickerPack(reference: .name("FestiveFontEmoji"), forceActualized: false)
|
||
)
|
||
|> map { animatedEmoji, numbers -> (FileMediaReference?, [FileMediaReference]) in
|
||
var effectFile: FileMediaReference?
|
||
if case let .result(_, items, _) = animatedEmoji {
|
||
let randomKey = ["🎉", "🎈", "🎆"].randomElement()!
|
||
outer: for item in items {
|
||
let indexKeys = item.getStringRepresentationsOfIndexKeys()
|
||
for key in indexKeys {
|
||
if key == randomKey {
|
||
effectFile = .stickerPack(stickerPack: .animatedEmojiAnimations, media: item.file._parse())
|
||
break outer
|
||
}
|
||
}
|
||
}
|
||
}
|
||
var numberFiles: [FileMediaReference] = []
|
||
if let age = ageForBirthday(birthday), case let .result(info, items, _) = numbers {
|
||
let ageKeys = ageToKeys(age)
|
||
for ageKey in ageKeys {
|
||
for item in items {
|
||
let indexKeys = item.getStringRepresentationsOfIndexKeys()
|
||
for key in indexKeys {
|
||
if key == ageKey {
|
||
numberFiles.append(.stickerPack(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), media: item.file._parse()))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return (effectFile, numberFiles)
|
||
}
|
||
|> deliverOnMainQueue).start(next: { [weak self] effectAndNumberFiles in
|
||
guard let self else {
|
||
return
|
||
}
|
||
let (effectFile, numberFiles) = effectAndNumberFiles
|
||
|
||
if let effectFile {
|
||
let _ = freeMediaFileInteractiveFetched(account: self.context.account, userLocation: .peer(self.context.account.peerId), fileReference: effectFile).startStandalone()
|
||
self.setupEffectAnimation(size: size, file: effectFile, sourceRect: sourceRect)
|
||
}
|
||
for file in numberFiles {
|
||
let _ = freeMediaFileInteractiveFetched(account: self.context.account, userLocation: .peer(self.context.account.peerId), fileReference: file).startStandalone()
|
||
}
|
||
self.setupNumberAnimations(size: size, files: numberFiles, sourceRect: sourceRect)
|
||
})
|
||
}
|
||
|
||
private func setupEffectAnimation(size: CGSize, file: FileMediaReference, sourceRect: CGRect?) {
|
||
guard let dimensions = file.media.dimensions else {
|
||
return
|
||
}
|
||
let minSide = min(size.width, size.height)
|
||
let pixelSize = dimensions.cgSize.aspectFitted(CGSize(width: 512.0, height: 512.0))
|
||
let animationSize = dimensions.cgSize.aspectFitted(CGSize(width: minSide, height: minSide))
|
||
|
||
let animationNode = DefaultAnimatedStickerNodeImpl()
|
||
let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.media.resource, fitzModifier: nil)
|
||
let pathPrefix = self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.media.resource.id)
|
||
animationNode.setup(source: source, width: Int(pixelSize.width), height: Int(pixelSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
|
||
self.addSubnode(animationNode)
|
||
|
||
let startY = sourceRect?.midY ?? size.height / 2.0
|
||
|
||
animationNode.updateLayout(size: animationSize)
|
||
animationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
|
||
animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: startY - animationSize.height / 2.0), size: animationSize)
|
||
animationNode.visibility = true
|
||
}
|
||
|
||
private func setupNumberAnimations(size: CGSize, files: [FileMediaReference], sourceRect: CGRect?) {
|
||
guard !files.isEmpty else {
|
||
return
|
||
}
|
||
|
||
let startY = sourceRect?.midY ?? 475.0
|
||
|
||
var offset: CGFloat = 0.0
|
||
var scaleDelay: Double = 0.0
|
||
if files.count > 1 {
|
||
offset -= 45.0
|
||
}
|
||
for file in files {
|
||
guard let dimensions = file.media.dimensions else {
|
||
continue
|
||
}
|
||
|
||
let animationSize = dimensions.cgSize.aspectFitted(CGSize(width: 144.0, height: 144.0))
|
||
let pixelSize = dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0))
|
||
|
||
let animationNode = DefaultAnimatedStickerNodeImpl()
|
||
let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.media.resource, fitzModifier: nil)
|
||
let pathPrefix: String? = self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.media.resource.id)
|
||
animationNode.setup(source: source, width: Int(pixelSize.width), height: Int(pixelSize.height), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix))
|
||
self.addSubnode(animationNode)
|
||
|
||
animationNode.updateLayout(size: animationSize)
|
||
animationNode.bounds = CGRect(origin: .zero, size: animationSize)
|
||
animationNode.transform = CATransform3DMakeScale(0.001, 0.001, 1.0)
|
||
animationNode.visibility = true
|
||
|
||
let path = UIBezierPath()
|
||
let startPoint = CGPoint(x: 90.0 + offset * 0.7, y: startY + CGFloat.random(in: -20.0 ..< 20.0))
|
||
animationNode.position = startPoint
|
||
path.move(to: startPoint)
|
||
path.addCurve(to: CGPoint(x: 205.0 + offset, y: -90.0), controlPoint1: CGPoint(x: 213.0 + offset * 0.8, y: 380.0 + CGFloat.random(in: -20.0 ..< 20.0)), controlPoint2: CGPoint(x: 206.0 + offset * 0.8, y: 134.0 + CGFloat.random(in: -20.0 ..< 20.0)))
|
||
|
||
let riseAnimation = CAKeyframeAnimation(keyPath: "position")
|
||
riseAnimation.path = path.cgPath
|
||
riseAnimation.duration = 2.2
|
||
riseAnimation.calculationMode = .cubic
|
||
riseAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn)
|
||
riseAnimation.beginTime = CACurrentMediaTime() + 0.5
|
||
riseAnimation.isRemovedOnCompletion = false
|
||
riseAnimation.fillMode = .forwards
|
||
riseAnimation.completion = { [weak self] _ in
|
||
self?.removeFromSupernode()
|
||
}
|
||
animationNode.layer.add(riseAnimation, forKey: "position")
|
||
offset += 132.0
|
||
|
||
let scaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
|
||
scaleAnimation.duration = 2.0
|
||
scaleAnimation.values = [0.0, 0.45, 1.0]
|
||
scaleAnimation.keyTimes = [0.0, 0.3, 1.0]
|
||
scaleAnimation.beginTime = CACurrentMediaTime() + scaleDelay
|
||
scaleAnimation.isRemovedOnCompletion = false
|
||
scaleAnimation.fillMode = .forwards
|
||
animationNode.layer.add(scaleAnimation, forKey: "scale")
|
||
scaleDelay += 0.3
|
||
}
|
||
}
|
||
|
||
static func preloadBirthdayAnimations(context: AccountContext, birthday: TelegramBirthday) {
|
||
let preload = combineLatest(
|
||
context.engine.stickers.loadedStickerPack(reference: .animatedEmojiAnimations, forceActualized: false),
|
||
context.engine.stickers.loadedStickerPack(reference: .name("FestiveFontEmoji"), forceActualized: false)
|
||
)
|
||
|> mapToSignal { animatedEmoji, numbers -> Signal<Never, NoError> in
|
||
var signals: [Signal<FetchResourceSourceType, FetchResourceError>] = []
|
||
if case let .result(_, items, _) = animatedEmoji {
|
||
for item in items {
|
||
let indexKeys = item.getStringRepresentationsOfIndexKeys()
|
||
for key in indexKeys {
|
||
if ["🎉", "🎈", "🎆"].contains(key) {
|
||
signals.append(freeMediaFileInteractiveFetched(account: context.account, userLocation: .peer(context.account.peerId), fileReference: .stickerPack(stickerPack: .animatedEmojiAnimations, media: item.file._parse())))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if let age = ageForBirthday(birthday), case let .result(info, items, _) = numbers {
|
||
let ageKeys = ageToKeys(age)
|
||
for item in items {
|
||
let indexKeys = item.getStringRepresentationsOfIndexKeys()
|
||
for key in indexKeys {
|
||
if ageKeys.contains(key) {
|
||
signals.append(freeMediaFileInteractiveFetched(account: context.account, userLocation: .peer(context.account.peerId), fileReference: .stickerPack(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), media: item.file._parse())))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if !signals.isEmpty {
|
||
return combineLatest(signals)
|
||
|> `catch` { _ in
|
||
return .complete()
|
||
}
|
||
|> ignoreValues
|
||
} else {
|
||
return .complete()
|
||
}
|
||
}
|
||
let _ = preload.startStandalone()
|
||
}
|
||
}
|
||
|
||
private func ageToKeys(_ age: Int) -> [String] {
|
||
return "\(age)".map { String($0) }.map {
|
||
switch $0 {
|
||
case "0":
|
||
return "0️⃣"
|
||
case "1":
|
||
return "1️⃣"
|
||
case "2":
|
||
return "2️⃣"
|
||
case "3":
|
||
return "3️⃣"
|
||
case "4":
|
||
return "4️⃣"
|
||
case "5":
|
||
return "5️⃣"
|
||
case "6":
|
||
return "6️⃣"
|
||
case "7":
|
||
return "7️⃣"
|
||
case "8":
|
||
return "8️⃣"
|
||
case "9":
|
||
return "9️⃣"
|
||
default:
|
||
return $0
|
||
}
|
||
}
|
||
}
|