2025-10-28 15:44:44 +04:00

198 lines
6.8 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramCore
import AvatarNode
import AppBundle
import AccountContext
import HierarchyTrackingLayer
private func makePeerBadgeImage(engine: TelegramEngine, peer: EnginePeer, count: Int) async -> UIImage {
let avatarSize: CGFloat = 16.0
let avatarInset: CGFloat = 2.0
let avatarIconSpacing: CGFloat = 2.0
let iconTextSpacing: CGFloat = 2.0
let iconSize: CGFloat = 8.0
let rightInset: CGFloat = 4.0
let text = NSAttributedString(string: "\(count)", font: Font.semibold(10.0), textColor: .white)
var textSize = text.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil).size
textSize.width = ceil(textSize.width)
textSize.height = ceil(textSize.height)
let size = CGSize(width: avatarInset + avatarSize + avatarIconSpacing + iconSize + iconTextSpacing + textSize.height + rightInset, height: avatarSize + avatarInset * 2.0)
return generateImage(size, rotatedContext: { size, context in
UIGraphicsPushContext(context)
defer {
UIGraphicsPopContext()
}
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: 0xFFB10D).cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: size.height * 0.5).cgPath)
context.fillPath()
text.draw(at: CGPoint(x: avatarInset + avatarSize + avatarIconSpacing + iconSize + iconTextSpacing, y: floorToScreenPixels((size.height - textSize.height) * 0.5)))
})!
}
private actor LiveChatReactionItemTaskQueue {
private final class PeerTask {
let peer: EnginePeer
let count: Int
let completion: (UIImage) -> Void
init(peer: EnginePeer, count: Int, completion: @escaping (UIImage) -> Void) {
self.peer = peer
self.count = count
self.completion = completion
}
}
private let engine: TelegramEngine
private var tasks: [PeerTask] = []
init(engine: TelegramEngine) {
self.engine = engine
}
func add(peer: EnginePeer, count: Int, completion: @escaping (UIImage) -> Void) {
self.tasks.append(PeerTask(peer: peer, count: count, completion: completion))
if self.tasks.count == 1 {
Task {
await processTasks()
}
}
}
private func processTasks() async {
while !self.tasks.isEmpty {
let task = self.tasks.removeFirst()
let image = await makePeerBadgeImage(engine: self.engine, peer: task.peer, count: task.count)
task.completion(image)
}
}
}
final class LiveChatReactionStreamView: UIView {
private final class ItemLayer: SimpleLayer {
init(image: UIImage) {
super.init()
self.contents = image.cgImage
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private var nextId: Int = 0
private var itemLayers: [Int: ItemLayer] = [:]
private let itemLayerContainer: SimpleLayer
private let hierarchyTracker: HierarchyTrackingLayer
private var previousTimestamp: Double = 0.0
private var displayLink: SharedDisplayLinkDriver.Link?
private var previousPhysicsTimestamp: Double = 0.0
private let taskQueue: LiveChatReactionItemTaskQueue
init(context: AccountContext) {
self.itemLayerContainer = SimpleLayer()
self.hierarchyTracker = HierarchyTrackingLayer()
self.taskQueue = LiveChatReactionItemTaskQueue(engine: context.engine)
super.init(frame: CGRect())
self.layer.addSublayer(self.itemLayerContainer)
self.layer.addSublayer(self.hierarchyTracker)
self.hierarchyTracker.isInHierarchyUpdated = { [weak self] inHierarchy in
guard let self else {
return
}
if inHierarchy {
if self.displayLink == nil {
self.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in
guard let self else {
return
}
self.updatePhysics()
})
}
} else {
self.displayLink = nil
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func add(peer: EnginePeer, count: Int) {
if !self.hierarchyTracker.isInHierarchy {
return
}
let timestamp = CFAbsoluteTimeGetCurrent()
if timestamp < self.previousTimestamp + 1.0 / 30.0 {
return
}
self.previousTimestamp = timestamp
Task {
await self.taskQueue.add(peer: peer, count: count, completion: { [weak self] image in
Task { @MainActor in
guard let self else {
return
}
self.addRenderedItem(image: image)
}
})
}
}
private func addRenderedItem(image: UIImage) {
if "".isEmpty {
return
}
let id = self.nextId
self.nextId += 1
let itemLayer = ItemLayer(image: image)
itemLayer.frame = CGRect(origin: CGPoint(x: -image.size.width - 10.0, y: -image.size.height * 0.5), size: image.size)
self.itemLayers[id] = itemLayer
self.itemLayerContainer.addSublayer(itemLayer)
let transition = ComponentTransition(animation: .curve(duration: 2.0, curve: .linear))
transition.setPosition(layer: itemLayer, position: CGPoint(x: itemLayer.position.x, y: -300.0))
itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, delay: 2.0 - 0.18, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
if let itemLayer = self.itemLayers[id] {
self.itemLayers.removeValue(forKey: id)
itemLayer.removeFromSuperlayer()
}
})
}
private func updatePhysics() {
let timestamp = CACurrentMediaTime()
let dt = max(1.0 / 120.0, min(1.0 / 30.0, timestamp - self.previousPhysicsTimestamp))
self.previousPhysicsTimestamp = timestamp
let _ = dt
}
func update(size: CGSize, sourcePoint: CGPoint, transition: ComponentTransition) {
transition.setFrame(layer: self.itemLayerContainer, frame: CGRect(origin: sourcePoint, size: CGSize()))
}
}