Implement knockout radial status node

This commit is contained in:
Ali 2019-11-15 20:39:47 +04:00
parent 4b4f13386c
commit 892bf598fb
3 changed files with 562 additions and 12 deletions

View File

@ -0,0 +1,17 @@
load("//Config:buck_rule_macros.bzl", "static_library")
static_library(
name = "SemanticStatusNode",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/Display:Display#shared",
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
"//submodules/LegacyComponents:LegacyComponents",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
],
)

View File

@ -0,0 +1,531 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
public enum SemanticStatusNodeState: Equatable {
case none
case download
case play
case pause
case progress(value: CGFloat?, cancelEnabled: Bool)
case customIcon(UIImage)
}
private protocol SemanticStatusNodeStateDrawingState: NSObjectProtocol {
func draw(context: CGContext, size: CGSize)
}
private protocol SemanticStatusNodeStateContext: class {
var isAnimating: Bool { get }
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState
}
private enum SemanticStatusNodeIcon: Equatable {
case none
case download
case play
case pause
case custom(UIImage)
}
private func svgPath(_ path: StaticString, scale: CGPoint = CGPoint(x: 1.0, y: 1.0), offset: CGPoint = CGPoint()) throws -> UIBezierPath {
var index: UnsafePointer<UInt8> = path.utf8Start
let end = path.utf8Start.advanced(by: path.utf8CodeUnitCount)
let path = UIBezierPath()
while index < end {
let c = index.pointee
index = index.successor()
if c == 77 { // M
let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
path.move(to: CGPoint(x: x, y: y))
} else if c == 76 { // L
let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
path.addLine(to: CGPoint(x: x, y: y))
} else if c == 67 { // C
let x1 = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y1 = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
let x2 = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y2 = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
path.addCurve(to: CGPoint(x: x, y: y), controlPoint1: CGPoint(x: x1, y: y1), controlPoint2: CGPoint(x: x2, y: y2))
} else if c == 32 { // space
continue
}
}
return path
}
private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContext {
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
let transitionFraction: CGFloat
let icon: SemanticStatusNodeIcon
init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon) {
self.transitionFraction = transitionFraction
self.icon = icon
super.init()
}
func draw(context: CGContext, size: CGSize) {
context.saveGState()
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction))
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.setBlendMode(.destinationOut)
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
switch self.icon {
case .none:
break
case .play:
let diameter = size.width
let factor = diameter / 50.0
let size = CGSize(width: 15.0, height: 18.0)
context.translateBy(x: (diameter - size.width) / 2.0 + 1.5, y: (diameter - size.height) / 2.0)
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: factor, y: factor)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ")
context.fillPath()
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
context.translateBy(x: -(diameter - size.width) / 2.0 - 1.5, y: -(diameter - size.height) / 2.0)
case .pause:
let diameter = size.width
let factor = diameter / 50.0
let size = CGSize(width: 15.0, height: 16.0)
context.translateBy(x: (diameter - size.width) / 2.0, y: (diameter - size.height) / 2.0)
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: factor, y: factor)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
let _ = try? drawSvgPath(context, path: "M0,1.00087166 C0,0.448105505 0.443716645,0 0.999807492,0 L4.00019251,0 C4.55237094,0 5,0.444630861 5,1.00087166 L5,14.9991283 C5,15.5518945 4.55628335,16 4.00019251,16 L0.999807492,16 C0.447629061,16 0,15.5553691 0,14.9991283 L0,1.00087166 Z M10,1.00087166 C10,0.448105505 10.4437166,0 10.9998075,0 L14.0001925,0 C14.5523709,0 15,0.444630861 15,1.00087166 L15,14.9991283 C15,15.5518945 14.5562834,16 14.0001925,16 L10.9998075,16 C10.4476291,16 10,15.5553691 10,14.9991283 L10,1.00087166 ")
context.fillPath()
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
context.translateBy(x: -(diameter - size.width) / 2.0, y: -(diameter - size.height) / 2.0)
case let .custom(image):
let diameter = size.width
let imageRect = CGRect(origin: CGPoint(x: floor((diameter - image.size.width) / 2.0), y: floor((diameter - image.size.height) / 2.0)), size: image.size)
context.saveGState()
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.clip(to: imageRect, mask: image.cgImage!)
context.fill(imageRect)
context.restoreGState()
case .download:
let diameter = size.width
let factor = diameter / 50.0
let lineWidth: CGFloat = max(1.6, 2.25 * factor)
context.setLineWidth(lineWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
let arrowHeadSize: CGFloat = 15.0 * factor
let arrowLength: CGFloat = 18.0 * factor
let arrowHeadOffset: CGFloat = 1.0 * factor
let leftPath = UIBezierPath()
leftPath.lineWidth = lineWidth
leftPath.lineCapStyle = .round
leftPath.lineJoinStyle = .round
leftPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset))
leftPath.addLine(to: CGPoint(x: diameter / 2.0 - arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset))
leftPath.stroke()
let rightPath = UIBezierPath()
rightPath.lineWidth = lineWidth
rightPath.lineCapStyle = .round
rightPath.lineJoinStyle = .round
rightPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset))
rightPath.addLine(to: CGPoint(x: diameter / 2.0 + arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset))
rightPath.stroke()
let bodyPath = UIBezierPath()
bodyPath.lineWidth = lineWidth
bodyPath.lineCapStyle = .round
bodyPath.lineJoinStyle = .round
bodyPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 - arrowLength / 2.0))
bodyPath.addLine(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0))
bodyPath.stroke()
}
context.restoreGState()
}
}
let icon: SemanticStatusNodeIcon
init(icon: SemanticStatusNodeIcon) {
self.icon = icon
}
var isAnimating: Bool {
return false
}
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
return DrawingState(transitionFraction: transitionFraction, icon: self.icon)
}
}
private final class SemanticStatusNodeProgressTransition {
let beginTime: Double
let initialValue: CGFloat
init(beginTime: Double, initialValue: CGFloat) {
self.beginTime = beginTime
self.initialValue = initialValue
}
func valueAt(timestamp: Double, actualValue: CGFloat) -> CGFloat {
let duration = 0.2
var t = CGFloat((timestamp - self.beginTime) / duration)
t = min(1.0, max(0.0, t))
return t * actualValue + (1.0 - t) * self.initialValue
}
}
private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext {
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
let transitionFraction: CGFloat
let value: CGFloat?
let displayCancel: Bool
let timestamp: Double
init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, timestamp: Double) {
self.transitionFraction = transitionFraction
self.value = value
self.displayCancel = displayCancel
self.timestamp = timestamp
super.init()
}
func draw(context: CGContext, size: CGSize) {
let diameter = size.width
let factor = diameter / 50.0
context.saveGState()
context.setBlendMode(.destinationOut)
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
var progress = self.value ?? 0.1
var startAngle = -CGFloat.pi / 2.0
var endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
if progress > 1.0 {
progress = 2.0 - progress
let tmp = startAngle
startAngle = endAngle
endAngle = tmp
}
progress = min(1.0, progress)
let lineWidth: CGFloat = max(1.6, 2.25 * factor)
let pathDiameter: CGFloat
pathDiameter = diameter - lineWidth - 2.5 * 2.0
var angle = self.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0)
angle *= 4.0
context.translateBy(x: diameter / 2.0, y: diameter / 2.0)
context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0)))
context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0)
let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = lineWidth
path.lineCapStyle = .round
path.stroke()
context.restoreGState()
if self.displayCancel {
context.setBlendMode(.destinationOut)
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
context.saveGState()
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction))
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.setLineWidth(max(1.3, 2.0 * factor))
context.setLineCap(.round)
let crossSize: CGFloat = 14.0 * factor
context.move(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0))
context.addLine(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0))
context.strokePath()
context.move(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0))
context.addLine(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0))
context.strokePath()
context.restoreGState()
}
}
}
var value: CGFloat?
let displayCancel: Bool
var transition: SemanticStatusNodeProgressTransition?
var isAnimating: Bool {
return true
}
init(value: CGFloat?, displayCancel: Bool) {
self.value = value
self.displayCancel = displayCancel
}
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
let timestamp = CACurrentMediaTime()
let resolvedValue: CGFloat?
if let value = self.value {
if let transition = self.transition {
transition.valueAt(timestamp: timestamp, actualValue: value)
} else {
resolvedValue = value
}
} else {
resolvedValue = nil
}
return DrawingState(transitionFraction: transitionFraction, value: self.value, displayCancel: self.displayCancel, timestamp: timestamp)
}
func updateValue(value: CGFloat?) {
if value != self.value {
let previousValue = value
self.value = value
let timestamp = CACurrentMediaTime()
if let value = value, let previousValue = previousValue {
if let transition = self.transition {
self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: transition.valueAt(timestamp: timestamp, actualValue: value))
} else {
self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: previousValue)
}
} else {
self.transition = nil
}
}
}
}
private extension SemanticStatusNodeState {
func context(current: SemanticStatusNodeStateContext?) -> SemanticStatusNodeStateContext {
switch self {
case .none, .download, .play, .pause, .customIcon:
let icon: SemanticStatusNodeIcon
switch self {
case .none:
icon = .none
case .download:
icon = .download
case .play:
icon = .play
case .pause:
icon = .pause
case let .customIcon(image):
icon = .custom(image)
default:
preconditionFailure()
}
if let current = current as? SemanticStatusNodeIconContext, current.icon == icon {
return current
} else {
return SemanticStatusNodeIconContext(icon: icon)
}
case let .progress(value, cancelEnabled):
if let current = current as? SemanticStatusNodeProgressContext, current.displayCancel == cancelEnabled {
current.value = value
return current
} else {
return SemanticStatusNodeProgressContext(value: value, displayCancel: cancelEnabled)
}
}
}
}
private final class SemanticStatusNodeTransitionDrawingState {
let transition: CGFloat
let drawingState: SemanticStatusNodeStateDrawingState
init(transition: CGFloat, drawingState: SemanticStatusNodeStateDrawingState) {
self.transition = transition
self.drawingState = drawingState
}
}
private final class SemanticStatusNodeDrawingState: NSObject {
let background: UIColor
let transitionState: SemanticStatusNodeTransitionDrawingState?
let drawingState: SemanticStatusNodeStateDrawingState
init(background: UIColor, transitionState: SemanticStatusNodeTransitionDrawingState?, drawingState: SemanticStatusNodeStateDrawingState) {
self.background = background
self.transitionState = transitionState
self.drawingState = drawingState
super.init()
}
}
private final class SemanticStatusNodeTransitionContext {
let startTime: Double
let duration: Double
let previousStateContext: SemanticStatusNodeStateContext
let completion: () -> Void
init(startTime: Double, duration: Double, previousStateContext: SemanticStatusNodeStateContext, completion: @escaping () -> Void) {
self.startTime = startTime
self.duration = duration
self.previousStateContext = previousStateContext
self.completion = completion
}
}
public final class SemanticStatusNode: ASControlNode {
public var backgroundNodeColor: UIColor {
didSet {
self.setNeedsDisplay()
}
}
private var animator: ConstantDisplayLinkAnimator?
public private(set) var state: SemanticStatusNodeState
private var transtionContext: SemanticStatusNodeTransitionContext?
private var stateContext: SemanticStatusNodeStateContext
public init(backgroundNodeColor: UIColor) {
self.backgroundNodeColor = backgroundNodeColor
self.state = .none
self.stateContext = self.state.context(current: nil)
super.init()
self.isOpaque = false
self.displaysAsynchronously = true
}
private func updateAnimations() {
var animate = false
let timestamp = CACurrentMediaTime()
if let transtionContext = self.transtionContext {
if transtionContext.startTime + transtionContext.duration < timestamp {
self.transtionContext = nil
transtionContext.completion()
} else {
animate = true
}
}
if self.stateContext.isAnimating {
animate = true
}
if animate {
let animator: ConstantDisplayLinkAnimator
if let current = self.animator {
animator = current
} else {
animator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.updateAnimations()
})
self.animator = animator
}
animator.isPaused = false
} else {
self.animator?.isPaused = true
}
self.setNeedsDisplay()
}
public func transitionToState(_ state: SemanticStatusNodeState, animated: Bool = true, synchronous: Bool = false, completion: @escaping () -> Void = {}) {
var animated = animated
if self.state != state {
let fromState = self.state
self.state = state
let previousStateContext = self.stateContext
self.stateContext = self.state.context(current: self.stateContext)
if animated && previousStateContext !== self.stateContext {
self.transtionContext = SemanticStatusNodeTransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousStateContext: previousStateContext, completion: completion)
} else {
completion()
}
self.updateAnimations()
self.setNeedsDisplay()
} else {
completion()
}
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
var transitionState: SemanticStatusNodeTransitionDrawingState?
var transitionFraction: CGFloat = 1.0
if let transitionContext = self.transtionContext {
let timestamp = CACurrentMediaTime()
var t = CGFloat((timestamp - transitionContext.startTime) / transitionContext.duration)
t = min(1.0, max(0.0, t))
transitionFraction = t
transitionState = SemanticStatusNodeTransitionDrawingState(transition: t, drawingState: transitionContext.previousStateContext.drawingState(transitionFraction: 1.0 - t))
}
return SemanticStatusNodeDrawingState(background: self.backgroundNodeColor, transitionState: transitionState, drawingState: self.stateContext.drawingState(transitionFraction: transitionFraction))
}
@objc override public 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? SemanticStatusNodeDrawingState else {
return
}
context.setFillColor(parameters.background.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: bounds.size))
if let transitionState = parameters.transitionState {
transitionState.drawingState.draw(context: context, size: bounds.size)
}
parameters.drawingState.draw(context: context, size: bounds.size)
}
}

View File

@ -9,9 +9,10 @@ import SyncCore
import UniversalMediaPlayer import UniversalMediaPlayer
import TelegramPresentationData import TelegramPresentationData
import AccountContext import AccountContext
import RadialStatusNode
import PhotoResources import PhotoResources
import TelegramStringFormatting import TelegramStringFormatting
import RadialStatusNode
import SemanticStatusNode
private struct FetchControls { private struct FetchControls {
let fetch: () -> Void let fetch: () -> Void
@ -35,7 +36,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
private let consumableContentNode: ASImageNode private let consumableContentNode: ASImageNode
private var iconNode: TransformImageNode? private var iconNode: TransformImageNode?
private var statusNode: RadialStatusNode? private var statusNode: SemanticStatusNode?
private var streamingStatusNode: RadialStatusNode? private var streamingStatusNode: RadialStatusNode?
private var tapRecognizer: UITapGestureRecognizer? private var tapRecognizer: UITapGestureRecognizer?
@ -435,7 +436,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
if hasThumbnail { if hasThumbnail {
fileIconImage = nil fileIconImage = nil
} else { } else {
let principalGraphics = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, gradientBubbles: context.sharedContext.immediateExperimentalUISettings.gradientBubbles) let principalGraphics = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
fileIconImage = incoming ? principalGraphics.radialIndicatorFileIconIncoming : principalGraphics.radialIndicatorFileIconOutgoing fileIconImage = incoming ? principalGraphics.radialIndicatorFileIconIncoming : principalGraphics.radialIndicatorFileIconOutgoing
} }
@ -609,9 +610,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status, actualFetchStatus in strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status, actualFetchStatus in
displayLinkDispatcher.dispatch { displayLinkDispatcher.dispatch {
if let strongSelf = strongSelf { if let strongSelf = strongSelf {
let firstTime = strongSelf.resourceStatus == nil
strongSelf.resourceStatus = status strongSelf.resourceStatus = status
strongSelf.actualFetchStatus = actualFetchStatus strongSelf.actualFetchStatus = actualFetchStatus
strongSelf.updateStatus(animated: !synchronousLoads) strongSelf.updateStatus(animated: !synchronousLoads || !firstTime)
} }
} }
})) }))
@ -687,7 +689,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
} }
} }
let state: RadialStatusNodeState let state: SemanticStatusNodeState
var streamingState: RadialStatusNodeState = .none var streamingState: RadialStatusNodeState = .none
let isSending = message.flags.isSending let isSending = message.flags.isSending
@ -768,10 +770,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
switch fetchStatus { switch fetchStatus {
case let .Fetching(_, progress): case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027) let adjustedProgress = max(progress, 0.027)
state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true) state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true)
case .Local: case .Local:
if isAudio { if isAudio {
state = .play(statusForegroundColor) state = .play
} else if let fileIconImage = self.fileIconImage { } else if let fileIconImage = self.fileIconImage {
state = .customIcon(fileIconImage) state = .customIcon(fileIconImage)
} else { } else {
@ -779,18 +781,18 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
} }
case .Remote: case .Remote:
if isAudio && !isVoice { if isAudio && !isVoice {
state = .play(statusForegroundColor) state = .play
} else { } else {
state = .download(statusForegroundColor) state = .download
} }
} }
case let .playbackStatus(playbackStatus): case let .playbackStatus(playbackStatus):
self.waveformScrubbingNode?.enableScrubbing = true self.waveformScrubbingNode?.enableScrubbing = true
switch playbackStatus { switch playbackStatus {
case .playing: case .playing:
state = .pause(statusForegroundColor) state = .pause
case .paused: case .paused:
state = .play(statusForegroundColor) state = .play
} }
} }
@ -802,7 +804,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
} }
if state != .none && self.statusNode == nil { if state != .none && self.statusNode == nil {
let statusNode = RadialStatusNode(backgroundNodeColor: backgroundNodeColor) let statusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor)
self.statusNode = statusNode self.statusNode = statusNode
statusNode.frame = progressFrame statusNode.frame = progressFrame
self.addSubnode(statusNode) self.addSubnode(statusNode)