Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,157 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
private func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat {
return (1.0 - value) * from + value * to
}
private final class ChatChoosingStickerActivityIndicatorNodeParameters: NSObject {
let color: UIColor
let progress: CGFloat
init(color: UIColor, progress: CGFloat) {
self.color = color
self.progress = progress
}
}
private class ChatChoosingStickerActivityIndicatorNode: ChatTitleActivityIndicatorNode {
override var duration: CFTimeInterval {
return 2.0
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
if let color = self.color {
return ChatChoosingStickerActivityIndicatorNodeParameters(color: color, progress: self.progress)
} else {
return nil
}
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? ChatChoosingStickerActivityIndicatorNodeParameters else {
return
}
context.setFillColor(UIColor.red.cgColor)
// context.fill(bounds)
let color = parameters.color
context.setFillColor(color.cgColor)
context.setStrokeColor(color.cgColor)
var heightProgress: CGFloat = parameters.progress * 4.0
if heightProgress > 3.0 {
heightProgress = 4.0 - heightProgress
} else if heightProgress > 2.0 {
heightProgress = heightProgress - 2.0
heightProgress *= heightProgress
} else if heightProgress > 1.0 {
heightProgress = 2.0 - heightProgress
} else {
heightProgress *= heightProgress
}
var pupilProgress: CGFloat = parameters.progress * 4.0
if pupilProgress > 2.0 {
pupilProgress = 3.0 - pupilProgress
}
pupilProgress = min(1.0, max(0.0, pupilProgress))
pupilProgress *= pupilProgress
var positionProgress: CGFloat = parameters.progress * 2.0
if positionProgress > 1.0 {
positionProgress = 2.0 - positionProgress
}
let eyeWidth: CGFloat = 6.0
let eyeHeight: CGFloat = 11.0 - 2.0 * heightProgress
let eyeOffset: CGFloat = -1.0 + positionProgress * 2.0
let leftCenter = CGPoint(x: bounds.width / 2.0 - eyeWidth - 1.0 + eyeOffset, y: bounds.height / 2.0)
let rightCenter = CGPoint(x: bounds.width / 2.0 + 1.0 + eyeOffset, y: bounds.height / 2.0)
let pupilSize: CGFloat = 4.0
let pupilCenter = CGPoint(x: -1.0 + pupilProgress * 2.0, y: 0.0)
context.strokeEllipse(in: CGRect(x: leftCenter.x - eyeWidth / 2.0, y: leftCenter.y - eyeHeight / 2.0, width: eyeWidth, height: eyeHeight))
context.fillEllipse(in: CGRect(x: leftCenter.x - pupilSize / 2.0 + pupilCenter.x * eyeWidth / 4.0, y: leftCenter.y - pupilSize / 2.0, width: pupilSize, height: pupilSize))
context.strokeEllipse(in: CGRect(x: rightCenter.x - eyeWidth / 2.0, y: rightCenter.y - eyeHeight / 2.0, width: eyeWidth, height: eyeHeight))
context.fillEllipse(in: CGRect(x: rightCenter.x - pupilSize / 2.0 + pupilCenter.x * eyeWidth / 4.0, y: rightCenter.y - pupilSize / 2.0, width: pupilSize, height: pupilSize))
}
}
class ChatChoosingStickerActivityContentNode: ChatTitleActivityContentNode {
private let indicatorNode: ChatChoosingStickerActivityIndicatorNode
private let advanced: Bool
init(text: NSAttributedString, color: UIColor) {
self.indicatorNode = ChatChoosingStickerActivityIndicatorNode(color: color)
var text = text
self.advanced = text.string == "choosing a sticker"
if self.advanced {
let mutable = text.mutableCopy() as? NSMutableAttributedString
mutable?.replaceCharacters(in: NSMakeRange(2, 2), with: " ")
if let updated = mutable{
text = updated
}
}
super.init(text: text)
self.addSubnode(self.indicatorNode)
}
override func updateLayout(_ constrainedSize: CGSize, offset: CGFloat, alignment: NSTextAlignment) -> CGSize {
let size = self.textNode.updateLayout(constrainedSize)
let scale = size.height / 15.0
let indicatorSize = CGSize(width: 24.0, height: 16.0)
let originX: CGFloat
let indicatorOriginX: CGFloat
if case .center = alignment {
if self.advanced {
originX = floorToScreenPixels((-size.width) / 2.0)
} else {
originX = floorToScreenPixels((indicatorSize.width - size.width) / 2.0)
}
} else {
if self.advanced {
originX = 4.0
} else {
originX = indicatorSize.width * scale - 1.0
}
}
self.textNode.frame = CGRect(origin: CGPoint(x: originX, y: 0.0), size: size)
if self.advanced {
if case .center = alignment {
indicatorOriginX = self.textNode.frame.minX + 26.0 + UIScreenPixel
} else {
var scale = scale
if scale > 1.25 {
scale *= 0.95
}
indicatorOriginX = self.textNode.frame.minX + floorToScreenPixels(26.0 * scale) + UIScreenPixel
}
} else {
indicatorOriginX = self.textNode.frame.minX - (indicatorSize.width * scale) / 2.0 + 3.0
}
self.indicatorNode.bounds = CGRect(origin: CGPoint(), size: indicatorSize)
self.indicatorNode.position = CGPoint(x: indicatorOriginX, y: size.height / 2.0)
self.indicatorNode.transform = CATransform3DMakeScale(scale, scale, 1.0)
return CGSize(width: size.width + indicatorSize.width, height: size.height)
}
}
@@ -0,0 +1,116 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
private func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat {
return (1.0 - value) * from + value * to
}
private final class ChatPlayingActivityIndicatorNodeParameters: NSObject {
let color: UIColor
let progress: CGFloat
init(color: UIColor, progress: CGFloat) {
self.color = color
self.progress = progress
}
}
private class ChatPlayingActivityIndicatorNode: ChatTitleActivityIndicatorNode {
override var duration: CFTimeInterval {
return 0.9
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
if let color = self.color {
return ChatPlayingActivityIndicatorNodeParameters(color: color, progress: self.progress)
} else {
return nil
}
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? ChatPlayingActivityIndicatorNodeParameters else {
return
}
let color = parameters.color.withAlphaComponent(parameters.color.alpha * 0.5)
context.setFillColor(color.cgColor)
let distance: CGFloat = 4.0
var origin = CGPoint(x: (bounds.size.width - distance * 2.0) / 2.0 + 4.0, y: bounds.size.height / 2.0 + 1.0)
var radius: CGFloat = 1.0
let dotsProgress = CGFloat(Int(parameters.progress * 100.0) % 50) / 50.0
let dotsX: CGFloat = 1.5 + origin.x - distance * dotsProgress
context.fillEllipse(in: CGRect(x: dotsX - radius, y: origin.y - radius, width: radius * 2.0, height: radius * 2.0))
context.fillEllipse(in: CGRect(x: dotsX - radius + distance, y: origin.y - radius, width: radius * 2.0, height: radius * 2.0))
context.setAlpha(dotsProgress)
context.fillEllipse(in: CGRect(x: dotsX - radius + distance * 2.0, y: origin.y - radius, width: radius * 2.0, height: radius * 2.0))
context.setAlpha(1.0)
let angle: CGFloat = 42.0 * CGFloat.pi / 180.0
radius = 3.5
let closing = Int(parameters.progress * 4) % 2 == 1
var bite = CGFloat(Int(parameters.progress * 100.0) % 25) / 25.0
if closing {
bite = 1.0 - bite
}
var startAngle = interpolate(from: 0.0, to: -angle, value: bite)
var endAngle = interpolate(from: 0.0, to: angle, value: bite)
if bite < CGFloat.ulpOfOne {
startAngle = CGFloat.pi * 2
endAngle = 0.0
}
origin.x = radius + 4.5
context.setAlpha(1.0)
context.setFillColor(parameters.color.cgColor)
context.beginPath()
context.move(to: origin)
context.addArc(center: origin, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
context.fillPath()
}
}
class ChatPlayingActivityContentNode: ChatTitleActivityContentNode {
private let indicatorNode: ChatPlayingActivityIndicatorNode
init(text: NSAttributedString, color: UIColor) {
self.indicatorNode = ChatPlayingActivityIndicatorNode(color: color)
super.init(text: text)
self.addSubnode(self.indicatorNode)
}
override func updateLayout(_ constrainedSize: CGSize, offset: CGFloat, alignment: NSTextAlignment) -> CGSize {
let size = self.textNode.updateLayout(constrainedSize)
let indicatorSize = CGSize(width: 24.0, height: 16.0)
let originX: CGFloat
if case .center = alignment {
originX = floorToScreenPixels((indicatorSize.width - size.width) / 2.0)
} else {
originX = indicatorSize.width
}
self.textNode.frame = CGRect(origin: CGPoint(x: originX, y: 0.0), size: size)
self.indicatorNode.frame = CGRect(origin: CGPoint(x: self.textNode.frame.minX - indicatorSize.width, y: 0.0), size: indicatorSize)
return CGSize(width: size.width + indicatorSize.width, height: size.height)
}
}
@@ -0,0 +1,88 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
private final class ChatRecordingVideoActivityIndicatorNodeParameters: NSObject {
let color: UIColor
let progress: CGFloat
init(color: UIColor, progress: CGFloat) {
self.color = color
self.progress = progress
}
}
private class ChatRecordingVideoActivityIndicatorNode: ChatTitleActivityIndicatorNode {
override var duration: CFTimeInterval {
return 0.9
}
override var timingFunction: CAMediaTimingFunction {
return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
if let color = self.color {
return ChatRecordingVideoActivityIndicatorNodeParameters(color: color, progress: self.progress)
} else {
return nil
}
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? ChatRecordingVideoActivityIndicatorNodeParameters else {
return
}
context.setFillColor(parameters.color.cgColor)
var progress = parameters.progress
if progress < 0.5 {
progress /= 0.5
} else {
progress = (1.0 - progress) / 0.5
}
let alpha = 1.0 - progress * 0.6
let radius = 3.5 - progress * 0.66
context.setAlpha(alpha)
context.fillEllipse(in: CGRect(x: 16.0 - radius, y: 9.0 - radius, width: radius * 2.0, height: radius * 2.0))
}
}
class ChatRecordingVideoActivityContentNode: ChatTitleActivityContentNode {
private let indicatorNode: ChatRecordingVideoActivityIndicatorNode
init(text: NSAttributedString, color: UIColor) {
self.indicatorNode = ChatRecordingVideoActivityIndicatorNode(color: color)
super.init(text: text)
self.addSubnode(self.indicatorNode)
}
override func updateLayout(_ constrainedSize: CGSize, offset: CGFloat, alignment: NSTextAlignment) -> CGSize {
let size = self.textNode.updateLayout(constrainedSize)
let indicatorSize = CGSize(width: 24.0, height: 16.0)
let originX: CGFloat
if case .center = alignment {
originX = floorToScreenPixels((indicatorSize.width - size.width) / 2.0)
} else {
originX = indicatorSize.width
}
self.textNode.frame = CGRect(origin: CGPoint(x: originX, y: 0.0), size: size)
self.indicatorNode.frame = CGRect(origin: CGPoint(x: self.textNode.frame.minX - indicatorSize.width, y: 0.0), size: indicatorSize)
return CGSize(width: size.width + indicatorSize.width, height: size.height)
}
}
@@ -0,0 +1,106 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
private final class ChatRecordingVoiceActivityIndicatorNodeParameters: NSObject {
let color: UIColor
let progress: CGFloat
init(color: UIColor, progress: CGFloat) {
self.color = color
self.progress = progress
}
}
private class ChatRecordingVoiceActivityIndicatorNode: ChatTitleActivityIndicatorNode {
override var duration: CFTimeInterval {
return 0.7
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
if let color = self.color {
return ChatRecordingVoiceActivityIndicatorNodeParameters(color: color, progress: self.progress)
} else {
return nil
}
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? ChatRecordingVoiceActivityIndicatorNodeParameters else {
return
}
context.setStrokeColor(parameters.color.cgColor)
context.setLineCap(.round)
context.setLineWidth(2.0)
let delta: CGFloat = 5.0
let origin = CGPoint(x: 3.0, y: bounds.size.height / 2.0 + 1.0)
let angle = 18.0 * CGFloat.pi / 180.0
let progress = parameters.progress * delta
var radius = progress
var alpha = radius / (3.0 * delta)
alpha = 1.0 - pow(cos(alpha * CGFloat.pi), 50)
context.setAlpha(alpha)
context.beginPath()
context.addArc(center: origin, radius: radius, startAngle: -angle, endAngle: angle, clockwise: false)
context.strokePath()
radius = progress + delta
alpha = radius / (3.0 * delta)
alpha = 1.0 - pow(cos(alpha * CGFloat.pi), 10)
context.setAlpha(alpha)
context.beginPath()
context.addArc(center: origin, radius: radius, startAngle: -angle, endAngle: angle, clockwise: false)
context.strokePath()
radius = progress + delta * 2.0
alpha = radius / (3.0 * delta)
alpha = 1.0 - pow(cos(alpha * CGFloat.pi), 10)
context.setAlpha(alpha)
context.beginPath()
context.addArc(center: origin, radius: radius, startAngle: -angle, endAngle: angle, clockwise: false)
context.strokePath()
}
}
class ChatRecordingVoiceActivityContentNode: ChatTitleActivityContentNode {
private let indicatorNode: ChatRecordingVoiceActivityIndicatorNode
init(text: NSAttributedString, color: UIColor) {
self.indicatorNode = ChatRecordingVoiceActivityIndicatorNode(color: color)
super.init(text: text)
self.addSubnode(self.indicatorNode)
}
override func updateLayout(_ constrainedSize: CGSize, offset: CGFloat, alignment: NSTextAlignment) -> CGSize {
let size = self.textNode.updateLayout(constrainedSize)
let indicatorSize = CGSize(width: 24.0, height: 16.0)
let originX: CGFloat
if case .center = alignment {
originX = floorToScreenPixels((indicatorSize.width - size.width) / 2.0)
} else {
originX = indicatorSize.width
}
self.textNode.frame = CGRect(origin: CGPoint(x: originX, y: offset), size: size)
self.indicatorNode.frame = CGRect(origin: CGPoint(x: self.textNode.frame.minX - indicatorSize.width, y: 0.0), size: indicatorSize)
return CGSize(width: size.width + indicatorSize.width, height: size.height)
}
}
@@ -0,0 +1,135 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
private let transitionDuration = 0.2
private let animationKey = "animation"
public class ChatTitleActivityIndicatorNode: ASDisplayNode {
public var duration: CFTimeInterval {
return 0.0
}
public var timingFunction: CAMediaTimingFunction {
return CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
}
public var color: UIColor? {
didSet {
self.setNeedsDisplay()
}
}
public var progress: CGFloat = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
public init(color: UIColor) {
self.color = color
super.init()
self.isLayerBacked = true
self.displaysAsynchronously = true
self.isOpaque = false
}
deinit {
self.stopAnimation()
}
private func startAnimation() {
self.stopAnimation()
let animation = POPBasicAnimation()
animation.property = POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! ChatTitleActivityIndicatorNode).progress
}
property?.writeBlock = { node, values in
(node as! ChatTitleActivityIndicatorNode).progress = values!.pointee
}
property?.threshold = 0.01
}) as? POPAnimatableProperty
animation.fromValue = 0.0 as NSNumber
animation.toValue = 1.0 as NSNumber
animation.timingFunction = self.timingFunction
animation.duration = self.duration
animation.repeatForever = true
self.pop_add(animation, forKey: animationKey)
}
private func stopAnimation() {
self.pop_removeAnimation(forKey: animationKey)
}
override public func didEnterHierarchy() {
super.didEnterHierarchy()
self.startAnimation()
}
override public func didExitHierarchy() {
super.didExitHierarchy()
self.stopAnimation()
}
}
public class ChatTitleActivityContentNode: ASDisplayNode {
public let textNode: ImmediateTextNode
public init(text: NSAttributedString) {
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 1
self.textNode.isOpaque = false
super.init()
self.addSubnode(self.textNode)
self.textNode.attributedText = text
}
func makeCopy() -> ASDisplayNode {
let node = ASDisplayNode()
let textNode = self.textNode.makeCopy()
textNode.frame = self.textNode.frame
node.addSubnode(textNode)
node.frame = self.frame
return node
}
public func animateOut(to: ChatTitleActivityNodeState, style: ChatTitleActivityAnimationStyle, completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration, removeOnCompletion: false, completion: { _ in
completion()
})
if case .slide = style {
self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 14.0), duration: transitionDuration, additive: true)
}
}
public func animateIn(from: ChatTitleActivityNodeState, style: ChatTitleActivityAnimationStyle) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: transitionDuration)
if case .slide = style {
self.layer.animatePosition(from: CGPoint(x: 0.0, y: -14.0), to: CGPoint(), duration: transitionDuration, additive: true)
}
}
public func updateLayout(_ constrainedSize: CGSize, offset: CGFloat, alignment: NSTextAlignment) -> CGSize {
let size = self.textNode.updateLayout(constrainedSize)
self.textNode.bounds = CGRect(origin: CGPoint(), size: size)
if case .center = alignment {
self.textNode.position = CGPoint(x: 0.0, y: size.height / 2.0 + offset)
} else {
self.textNode.position = CGPoint(x: size.width / 2.0 + 3.0, y: size.height / 2.0 + offset)
}
return size
}
}
@@ -0,0 +1,135 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
public enum ChatTitleActivityAnimationStyle {
case none
case crossfade
case slide
}
public enum ChatTitleActivityInfoType {
case online
case lastSeenTime
case generic
}
public enum ChatTitleActivityNodeState: Equatable {
case none
case info(NSAttributedString, ChatTitleActivityInfoType)
case typingText(NSAttributedString, UIColor)
case uploading(NSAttributedString, UIColor)
case recordingVoice(NSAttributedString, UIColor)
case recordingVideo(NSAttributedString, UIColor)
case playingGame(NSAttributedString, UIColor)
case choosingSticker(NSAttributedString, UIColor)
case interactingWithEmoji(NSAttributedString, UIColor)
func contentNode() -> ChatTitleActivityContentNode? {
switch self {
case .none:
return nil
case let .info(text, _):
return ChatTitleActivityContentNode(text: text)
case let .typingText(text, color):
return ChatTypingActivityContentNode(text: text, color: color)
case let .uploading(text, color):
return ChatUploadingActivityContentNode(text: text, color: color)
case let .recordingVoice(text, color):
return ChatRecordingVoiceActivityContentNode(text: text, color: color)
case let .recordingVideo(text, color):
return ChatRecordingVideoActivityContentNode(text: text, color: color)
case let .playingGame(text, color):
return ChatPlayingActivityContentNode(text: text, color: color)
case let .choosingSticker(text, color):
return ChatChoosingStickerActivityContentNode(text: text, color: color)
case let .interactingWithEmoji(text, _):
return ChatTitleActivityContentNode(text: text)
}
}
public var string: String? {
if case let .info(text, _) = self {
return text.string
}
return nil
}
}
public class ChatTitleActivityNode: ASDisplayNode {
public private(set) var state: ChatTitleActivityNodeState = .none
private var contentNode: ChatTitleActivityContentNode?
private var nextContentNode: ChatTitleActivityContentNode?
override public init() {
super.init()
}
public func makeCopy() -> ASDisplayNode {
let node = ASDisplayNode()
if let contentNode = self.contentNode {
node.addSubnode(contentNode.makeCopy())
}
node.frame = self.frame
return node
}
public func transitionToState(_ state: ChatTitleActivityNodeState, animation: ChatTitleActivityAnimationStyle = .crossfade, completion: @escaping () -> Void = {}) -> Bool {
if self.state != state {
let currentState = self.state
self.state = state
let contentNode = state.contentNode()
if contentNode !== self.contentNode {
self.transitionToContentNode(contentNode, state: state, fromState: currentState, animation: animation, completion: completion)
}
return true
} else {
completion()
return false
}
}
private func transitionToContentNode(_ node: ChatTitleActivityContentNode?, state: ChatTitleActivityNodeState, fromState: ChatTitleActivityNodeState, animation: ChatTitleActivityAnimationStyle = .crossfade, completion: @escaping () -> Void) {
if let previousContentNode = self.contentNode {
if case .none = animation {
previousContentNode.removeFromSupernode()
self.contentNode = node
if let contentNode = self.contentNode {
self.addSubnode(contentNode)
}
} else {
var animation = animation
if case let .info(_, fromType) = fromState, case let .info(_, toType) = state, fromType == toType {
animation = .none
}
if case .typingText = fromState, case .typingText = state {
animation = .none
}
self.contentNode = node
if let contentNode = self.contentNode {
self.addSubnode(contentNode)
if self.isNodeLoaded {
contentNode.animateIn(from: fromState, style: animation)
}
}
previousContentNode.animateOut(to: state, style: animation) {
previousContentNode.removeFromSupernode()
}
}
} else {
self.contentNode = node
if let contentNode = self.contentNode {
self.addSubnode(contentNode)
}
}
}
public func updateLayout(_ constrainedSize: CGSize, offset: CGFloat = 0.0, alignment: NSTextAlignment) -> CGSize {
return CGSize(width: 0.0, height: self.contentNode?.updateLayout(constrainedSize, offset: offset, alignment: alignment).height ?? 0.0)
}
}
@@ -0,0 +1,126 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
private func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat {
return (1.0 - value) * from + value * to
}
private let minDiameter: CGFloat = 3.0
private let maxDiameter: CGFloat = 4.5
private func radiusFunction(value: CGFloat, timeOffset: CGFloat) -> CGFloat {
var clampedValue = value + timeOffset
if clampedValue > 1.0 {
clampedValue = clampedValue - floor(clampedValue)
}
if clampedValue < 0.4 {
return interpolate(from: minDiameter, to: maxDiameter, value: clampedValue / 0.4)
} else if clampedValue < 0.8 {
return interpolate(from: maxDiameter, to: minDiameter, value: (clampedValue - 0.4) / 0.4)
}
return minDiameter
}
private final class ChatTypingActivityIndicatorNodeParameters: NSObject {
let color: UIColor
let progress: CGFloat
init(color: UIColor, progress: CGFloat) {
self.color = color
self.progress = progress
}
}
private class ChatTypingActivityIndicatorNode: ChatTitleActivityIndicatorNode {
override var duration: CFTimeInterval {
return 0.7
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
if let color = self.color {
return ChatTypingActivityIndicatorNodeParameters(color: color, progress: self.progress)
} else {
return nil
}
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? ChatTypingActivityIndicatorNodeParameters else {
return
}
let leftPadding: CGFloat = 6.0
let topPadding: CGFloat = 9.0
let distance: CGFloat = 11.0 / 2.0
let minAlpha: CGFloat = 0.75
let deltaAlpha: CGFloat = 1.0 - minAlpha
var radius = radiusFunction(value: parameters.progress, timeOffset:0.4)
radius = (max(minDiameter, radius) - minDiameter) / (maxDiameter - minDiameter)
radius = radius * 1.5
let initialAlpha = parameters.color.alpha
var dotsColor = parameters.color.withAlphaComponent((radius * deltaAlpha + minAlpha) * initialAlpha)
context.setFillColor(dotsColor.cgColor)
context.fillEllipse(in: CGRect(x: leftPadding - minDiameter / 2.0 - radius / 2.0, y: topPadding - minDiameter / 2.0 - radius / 2.0, width: minDiameter + radius, height: minDiameter + radius))
radius = radiusFunction(value: parameters.progress, timeOffset: 0.2)
radius = (max(minDiameter, radius) - minDiameter) / (maxDiameter - minDiameter)
radius = radius * 1.5
dotsColor = parameters.color.withAlphaComponent((radius * deltaAlpha + minAlpha) * initialAlpha)
context.setFillColor(dotsColor.cgColor)
context.fillEllipse(in: CGRect(x: leftPadding + distance - minDiameter / 2.0 - radius / 2.0, y: topPadding - minDiameter / 2.0 - radius / 2.0, width: minDiameter + radius, height: minDiameter + radius))
radius = radiusFunction(value: parameters.progress, timeOffset: 0.0)
radius = (max(minDiameter, radius) - minDiameter) / (maxDiameter - minDiameter)
radius = radius * 1.5
dotsColor = parameters.color.withAlphaComponent((radius * deltaAlpha + minAlpha) * initialAlpha)
context.setFillColor(dotsColor.cgColor)
context.fillEllipse(in: CGRect(x: leftPadding + distance * 2.0 - minDiameter / 2.0 - radius / 2.0, y: topPadding - minDiameter / 2.0 - radius / 2.0, width: minDiameter + radius, height: minDiameter + radius))
}
}
class ChatTypingActivityContentNode: ChatTitleActivityContentNode {
private let indicatorNode: ChatTypingActivityIndicatorNode
init(text: NSAttributedString, color: UIColor) {
self.indicatorNode = ChatTypingActivityIndicatorNode(color: color)
super.init(text: text)
self.addSubnode(self.indicatorNode)
}
override func updateLayout(_ constrainedSize: CGSize, offset: CGFloat, alignment: NSTextAlignment) -> CGSize {
let indicatorSize = CGSize(width: 24.0, height: 16.0)
let size = self.textNode.updateLayout(CGSize(width: constrainedSize.width - indicatorSize.width, height: constrainedSize.height))
var originX: CGFloat
if case .center = alignment {
originX = floorToScreenPixels((indicatorSize.width - size.width) / 2.0)
let overflowX = max(0.0, size.width + indicatorSize.width + 8.0 - constrainedSize.width)
originX = originX + overflowX
} else {
originX = indicatorSize.width
}
self.textNode.frame = CGRect(origin: CGPoint(x: originX, y: 0.0), size: size)
self.indicatorNode.frame = CGRect(origin: CGPoint(x: self.textNode.frame.minX - indicatorSize.width, y: floorToScreenPixels((size.height - indicatorSize.height) / 2.0)), size: indicatorSize)
return CGSize(width: size.width + indicatorSize.width, height: size.height)
}
}
@@ -0,0 +1,96 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
private func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat {
return (1.0 - value) * from + value * to
}
private final class ChatUploadingActivityIndicatorNodeParameters: NSObject {
let color: UIColor
let progress: CGFloat
init(color: UIColor, progress: CGFloat) {
self.color = color
self.progress = progress
}
}
private class ChatUploadingActivityIndicatorNode: ChatTitleActivityIndicatorNode {
override var duration: CFTimeInterval {
return 1.75
}
override var timingFunction: CAMediaTimingFunction {
return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
if let color = self.color {
return ChatUploadingActivityIndicatorNodeParameters(color: color, progress: self.progress)
} else {
return nil
}
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? ChatUploadingActivityIndicatorNodeParameters else {
return
}
let origin = CGPoint(x: 4.0 + UIScreenPixel, y: 7.0)
let size = CGSize(width: 13.0, height: 4.0)
let radius: CGFloat = 1.25
var color = parameters.color.withAlphaComponent(parameters.color.alpha * 0.3)
context.setFillColor(color.cgColor)
var path = UIBezierPath(roundedRect: CGRect(origin: origin, size: size), cornerRadius: radius)
path.fill(with: .normal, alpha: 1.0)
path.addClip()
let progress = interpolate(from: 0.0, to: size.width * 2.0, value: parameters.progress)
color = parameters.color
context.setFillColor(color.cgColor)
path = UIBezierPath(roundedRect: CGRect(origin: origin.offsetBy(dx: -size.width + progress, dy: 0.0), size: size), cornerRadius: radius)
path.fill(with: .normal, alpha: 1.0)
}
}
class ChatUploadingActivityContentNode: ChatTitleActivityContentNode {
private let indicatorNode: ChatUploadingActivityIndicatorNode
init(text: NSAttributedString, color: UIColor) {
self.indicatorNode = ChatUploadingActivityIndicatorNode(color: color)
super.init(text: text)
self.addSubnode(self.indicatorNode)
}
override func updateLayout(_ constrainedSize: CGSize, offset: CGFloat, alignment: NSTextAlignment) -> CGSize {
let size = self.textNode.updateLayout(constrainedSize)
let indicatorSize = CGSize(width: 24.0, height: 16.0)
let originX: CGFloat
if case .center = alignment {
originX = floorToScreenPixels((indicatorSize.width - size.width) / 2.0)
} else {
originX = indicatorSize.width
}
self.textNode.frame = CGRect(origin: CGPoint(x: originX, y: 0.0), size: size)
self.indicatorNode.frame = CGRect(origin: CGPoint(x: self.textNode.frame.minX - indicatorSize.width, y: 0.0), size: indicatorSize)
return CGSize(width: size.width + indicatorSize.width, height: size.height)
}
}