mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Voice Chat UI fixes
This commit is contained in:
parent
de73c57d9d
commit
623882121b
@ -230,6 +230,15 @@ public struct PresentationGroupCallMemberState: Equatable {
|
|||||||
public enum PresentationGroupCallMuteAction: Equatable {
|
public enum PresentationGroupCallMuteAction: Equatable {
|
||||||
case muted(isPushToTalkActive: Bool)
|
case muted(isPushToTalkActive: Bool)
|
||||||
case unmuted
|
case unmuted
|
||||||
|
|
||||||
|
public var isEffectivelyMuted: Bool {
|
||||||
|
switch self {
|
||||||
|
case let .muted(isPushToTalkActive):
|
||||||
|
return !isPushToTalkActive
|
||||||
|
case .unmuted:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct PresentationGroupCallMembers: Equatable {
|
public struct PresentationGroupCallMembers: Equatable {
|
||||||
@ -265,7 +274,6 @@ public protocol PresentationGroupCall: class {
|
|||||||
var members: Signal<PresentationGroupCallMembers?, NoError> { get }
|
var members: Signal<PresentationGroupCallMembers?, NoError> { get }
|
||||||
var audioLevels: Signal<[(PeerId, Float)], NoError> { get }
|
var audioLevels: Signal<[(PeerId, Float)], NoError> { get }
|
||||||
var myAudioLevel: Signal<Float, NoError> { get }
|
var myAudioLevel: Signal<Float, NoError> { get }
|
||||||
var speakingAudioLevels: Signal<[(PeerId, Float)], NoError> { get }
|
|
||||||
var isMuted: Signal<Bool, NoError> { get }
|
var isMuted: Signal<Bool, NoError> { get }
|
||||||
|
|
||||||
func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError>
|
func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError>
|
||||||
|
@ -378,7 +378,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let panelData = currentState ?? availableState
|
let panelData = currentState != nil ? nil : availableState
|
||||||
|
|
||||||
let wasEmpty = strongSelf.groupCallPanelData == nil
|
let wasEmpty = strongSelf.groupCallPanelData == nil
|
||||||
strongSelf.groupCallPanelData = panelData
|
strongSelf.groupCallPanelData = panelData
|
||||||
|
@ -8,401 +8,128 @@ import TelegramCore
|
|||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
import LegacyComponents
|
||||||
|
|
||||||
private let colorSpace = CGColorSpaceCreateDeviceRGB()
|
private let blue = UIColor(rgb: 0x0078ff)
|
||||||
|
private let lightBlue = UIColor(rgb: 0x59c7f8)
|
||||||
private class CurveDrawingState: NSObject {
|
private let green = UIColor(rgb: 0x33c659)
|
||||||
let path: UIBezierPath
|
|
||||||
let offset: CGFloat
|
|
||||||
let alpha: CGFloat
|
|
||||||
|
|
||||||
init(path: UIBezierPath, offset: CGFloat, alpha: CGFloat) {
|
|
||||||
self.path = path
|
|
||||||
self.offset = offset
|
|
||||||
self.alpha = alpha
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CallStatusBarBackgroundNodeDrawingState: NSObject {
|
|
||||||
let timestamp: Double
|
|
||||||
let curves: [CurveDrawingState]
|
|
||||||
let speaking: Bool
|
|
||||||
let gradientTransition: CGFloat
|
|
||||||
let gradientMovement: CGFloat
|
|
||||||
|
|
||||||
init(timestamp: Double, curves: [CurveDrawingState], speaking: Bool, gradientTransition: CGFloat, gradientMovement: CGFloat) {
|
|
||||||
self.timestamp = timestamp
|
|
||||||
self.curves = curves
|
|
||||||
self.speaking = speaking
|
|
||||||
self.gradientTransition = gradientTransition
|
|
||||||
self.gradientMovement = gradientMovement
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class Curve {
|
|
||||||
let pointsCount: Int
|
|
||||||
let smoothness: CGFloat
|
|
||||||
|
|
||||||
let minRandomness: CGFloat
|
|
||||||
let maxRandomness: CGFloat
|
|
||||||
|
|
||||||
let minSpeed: CGFloat
|
|
||||||
let maxSpeed: CGFloat
|
|
||||||
|
|
||||||
var size: CGSize {
|
|
||||||
didSet {
|
|
||||||
if self.size != oldValue {
|
|
||||||
self.fromPoints = nil
|
|
||||||
self.toPoints = nil
|
|
||||||
self.animateToNewShape()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let alpha: CGFloat
|
|
||||||
var currentOffset: CGFloat = 1.0
|
|
||||||
var minOffset: CGFloat = 0.0
|
|
||||||
var maxOffset: CGFloat = 2.0
|
|
||||||
|
|
||||||
private var speedLevel: CGFloat = 0.0
|
|
||||||
private var lastSpeedLevel: CGFloat = 0.0
|
|
||||||
|
|
||||||
private var fromPoints: [CGPoint]?
|
|
||||||
private var toPoints: [CGPoint]?
|
|
||||||
|
|
||||||
private var currentPoints: [CGPoint]? {
|
|
||||||
guard let fromPoints = self.fromPoints, let toPoints = self.toPoints else { return nil }
|
|
||||||
|
|
||||||
return fromPoints.enumerated().map { offset, fromPoint in
|
|
||||||
let toPoint = toPoints[offset]
|
|
||||||
return CGPoint(x: fromPoint.x + (toPoint.x - fromPoint.x) * transition, y: fromPoint.y + (toPoint.y - fromPoint.y) * transition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentShape: UIBezierPath?
|
|
||||||
private var transition: CGFloat = 0 {
|
|
||||||
didSet {
|
|
||||||
if let currentPoints = self.currentPoints {
|
|
||||||
self.currentShape = UIBezierPath.smoothCurve(through: currentPoints, length: size.width, smoothness: smoothness, curve: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var level: CGFloat = 0.0 {
|
|
||||||
didSet {
|
|
||||||
self.currentOffset = min(self.maxOffset, max(self.minOffset, self.minOffset + (self.maxOffset - self.minOffset) * self.level))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var transitionArguments: (startTime: Double, duration: Double)?
|
|
||||||
|
|
||||||
var loop: Bool = true {
|
|
||||||
didSet {
|
|
||||||
if let _ = transitionArguments {
|
|
||||||
} else {
|
|
||||||
self.animateToNewShape()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(
|
|
||||||
size: CGSize,
|
|
||||||
alpha: CGFloat,
|
|
||||||
pointsCount: Int,
|
|
||||||
minRandomness: CGFloat,
|
|
||||||
maxRandomness: CGFloat,
|
|
||||||
minSpeed: CGFloat,
|
|
||||||
maxSpeed: CGFloat,
|
|
||||||
minOffset: CGFloat,
|
|
||||||
maxOffset: CGFloat
|
|
||||||
) {
|
|
||||||
self.size = size
|
|
||||||
self.alpha = alpha
|
|
||||||
self.pointsCount = pointsCount
|
|
||||||
self.minRandomness = minRandomness
|
|
||||||
self.maxRandomness = maxRandomness
|
|
||||||
self.minSpeed = minSpeed
|
|
||||||
self.maxSpeed = maxSpeed
|
|
||||||
self.minOffset = minOffset
|
|
||||||
self.maxOffset = maxOffset
|
|
||||||
|
|
||||||
self.smoothness = 0.35
|
|
||||||
|
|
||||||
self.currentOffset = minOffset
|
|
||||||
|
|
||||||
self.animateToNewShape()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateSpeedLevel(to newSpeedLevel: CGFloat) {
|
|
||||||
self.speedLevel = max(self.speedLevel, newSpeedLevel)
|
|
||||||
|
|
||||||
if abs(lastSpeedLevel - newSpeedLevel) > 0.3 {
|
|
||||||
animateToNewShape()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func animateToNewShape() {
|
|
||||||
if let _ = self.transitionArguments {
|
|
||||||
self.fromPoints = self.currentPoints
|
|
||||||
self.toPoints = nil
|
|
||||||
self.transition = 0.0
|
|
||||||
self.transitionArguments = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.fromPoints == nil {
|
|
||||||
self.fromPoints = generateNextCurve(for: self.size)
|
|
||||||
}
|
|
||||||
if self.toPoints == nil {
|
|
||||||
self.toPoints = generateNextCurve(for: self.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
let duration: Double = 1.0 / Double(minSpeed + (maxSpeed - minSpeed) * speedLevel)
|
|
||||||
self.transitionArguments = (CACurrentMediaTime(), duration)
|
|
||||||
|
|
||||||
self.lastSpeedLevel = self.speedLevel
|
|
||||||
self.speedLevel = 0
|
|
||||||
|
|
||||||
self.updateAnimations()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateAnimations() {
|
|
||||||
let timestamp = CACurrentMediaTime()
|
|
||||||
|
|
||||||
if let (startTime, duration) = self.transitionArguments, duration > 0.0 {
|
|
||||||
var t = max(0.0, min(1.0, CGFloat((timestamp - startTime) / duration)))
|
|
||||||
if t < 0.5 {
|
|
||||||
t = 2 * t * t
|
|
||||||
} else {
|
|
||||||
t = -1 + (4 - 2 * t) * t
|
|
||||||
}
|
|
||||||
self.transition = t
|
|
||||||
if self.transition < 1.0 {
|
|
||||||
} else {
|
|
||||||
if self.loop {
|
|
||||||
self.animateToNewShape()
|
|
||||||
} else {
|
|
||||||
self.fromPoints = self.currentPoints
|
|
||||||
self.toPoints = nil
|
|
||||||
self.transition = 0.0
|
|
||||||
self.transitionArguments = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateNextCurve(for size: CGSize) -> [CGPoint] {
|
|
||||||
let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel
|
|
||||||
return curve(pointsCount: pointsCount, randomness: randomness).map {
|
|
||||||
return CGPoint(x: $0.x * CGFloat(size.width), y: size.height - 18.0 + $0.y * 12.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func curve(pointsCount: Int, randomness: CGFloat) -> [CGPoint] {
|
|
||||||
let segment = 1.0 / CGFloat(pointsCount - 1)
|
|
||||||
|
|
||||||
let rgen = { () -> CGFloat in
|
|
||||||
let accuracy: UInt32 = 1000
|
|
||||||
let random = arc4random_uniform(accuracy)
|
|
||||||
return CGFloat(random) / CGFloat(accuracy)
|
|
||||||
}
|
|
||||||
let rangeStart: CGFloat = 1.0 / (1.0 + randomness / 10.0)
|
|
||||||
|
|
||||||
let points = (0 ..< pointsCount).map { i -> CGPoint in
|
|
||||||
let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2
|
|
||||||
let segmentRandomness: CGFloat = randomness
|
|
||||||
|
|
||||||
let pointX: CGFloat
|
|
||||||
let pointY: CGFloat
|
|
||||||
let randomXDelta: CGFloat
|
|
||||||
if i == 0 {
|
|
||||||
pointX = 0.0
|
|
||||||
pointY = 0.0
|
|
||||||
randomXDelta = 0.0
|
|
||||||
} else if i == pointsCount - 1 {
|
|
||||||
pointX = 1.0
|
|
||||||
pointY = 0.0
|
|
||||||
randomXDelta = 0.0
|
|
||||||
} else {
|
|
||||||
pointX = segment * CGFloat(i)
|
|
||||||
pointY = ((segmentRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - segmentRandomness * 0.5) * randPointOffset
|
|
||||||
randomXDelta = segment - segment * randPointOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
return CGPoint(x: pointX + randomXDelta, y: pointY)
|
|
||||||
}
|
|
||||||
|
|
||||||
return points
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CallStatusBarBackgroundNode: ASDisplayNode {
|
private class CallStatusBarBackgroundNode: ASDisplayNode {
|
||||||
var muted = true
|
private let foregroundView: UIView
|
||||||
|
private let foregroundGradientLayer: CAGradientLayer
|
||||||
|
private let maskCurveView: VoiceCurveView
|
||||||
|
|
||||||
var audioLevel: Float = 0.0 {
|
var audioLevel: Float = 0.0 {
|
||||||
didSet {
|
didSet {
|
||||||
for curve in self.curves {
|
self.maskCurveView.updateLevel(CGFloat(audioLevel))
|
||||||
curve.loop = audioLevel.isZero
|
|
||||||
curve.updateSpeedLevel(to: CGFloat(self.audioLevel))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
var presentationAudioLevel: CGFloat = 0.0
|
|
||||||
|
|
||||||
typealias CurveRange = (min: CGFloat, max: CGFloat)
|
|
||||||
let curves: [Curve]
|
|
||||||
|
|
||||||
private var gradientMovementArguments: (from: CGFloat, to: CGFloat, startTime: Double, duration: Double)?
|
|
||||||
private var gradientMovement: CGFloat = 0.0
|
|
||||||
|
|
||||||
var transitionArguments: (startTime: Double, duration: Double)?
|
|
||||||
var speaking = false {
|
var speaking = false {
|
||||||
didSet {
|
didSet {
|
||||||
if self.speaking != oldValue {
|
if self.speaking != oldValue {
|
||||||
self.transitionArguments = (CACurrentMediaTime(), 0.3)
|
let initialColors = self.foregroundGradientLayer.colors
|
||||||
|
let targetColors: [CGColor]
|
||||||
|
if speaking {
|
||||||
|
targetColors = [green.cgColor, blue.cgColor]
|
||||||
|
} else {
|
||||||
|
targetColors = [blue.cgColor, lightBlue.cgColor]
|
||||||
|
}
|
||||||
|
self.foregroundGradientLayer.colors = targetColors
|
||||||
|
self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var animator: ConstantDisplayLinkAnimator?
|
var isCurrentlyInHierarchy = false
|
||||||
|
override func didEnterHierarchy() {
|
||||||
|
super.didEnterHierarchy()
|
||||||
|
|
||||||
|
self.isCurrentlyInHierarchy = true
|
||||||
|
self.updateAnimations()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didExitHierarchy() {
|
||||||
|
super.didExitHierarchy()
|
||||||
|
|
||||||
|
self.isCurrentlyInHierarchy = false
|
||||||
|
self.updateAnimations()
|
||||||
|
}
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
let smallCurveRange: CurveRange = (0.0, 0.0)
|
self.foregroundView = UIView()
|
||||||
let mediumCurveRange: CurveRange = (0.1, 0.55)
|
self.foregroundGradientLayer = CAGradientLayer()
|
||||||
let bigCurveRange: CurveRange = (0.1, 1.0)
|
self.maskCurveView = VoiceCurveView(frame: CGRect(), maxLevel: 2.5, smallCurveRange: (0.0, 0.0), mediumCurveRange: (0.1, 0.55), bigCurveRange: (0.1, 1.0))
|
||||||
|
self.maskCurveView.setColor(UIColor(rgb: 0xffffff))
|
||||||
let size = CGSize(width: 375.0, height: 44.0)
|
|
||||||
let smallCurve = Curve(size: size, alpha: 1.0, pointsCount: 7, minRandomness: 1, maxRandomness: 1.3, minSpeed: 1.0, maxSpeed: 3.5, minOffset: smallCurveRange.min, maxOffset: smallCurveRange.max)
|
|
||||||
let mediumCurve = Curve(size: size, alpha: 0.55, pointsCount: 7, minRandomness: 1.2, maxRandomness: 1.5, minSpeed: 1.0, maxSpeed: 4.5, minOffset: mediumCurveRange.min, maxOffset: mediumCurveRange.max)
|
|
||||||
let largeCurve = Curve(size: size, alpha: 0.35, pointsCount: 7, minRandomness: 1.2, maxRandomness: 1.7, minSpeed: 1.0, maxSpeed: 6.0, minOffset: bigCurveRange.min, maxOffset: bigCurveRange.max)
|
|
||||||
|
|
||||||
self.curves = [smallCurve, mediumCurve, largeCurve]
|
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
self.foregroundGradientLayer.colors = [blue.cgColor, lightBlue.cgColor]
|
||||||
|
self.foregroundGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
|
||||||
|
self.foregroundGradientLayer.endPoint = CGPoint(x: 2.0, y: 0.5)
|
||||||
|
|
||||||
|
self.foregroundView.mask = self.maskCurveView
|
||||||
|
|
||||||
self.isOpaque = false
|
self.isOpaque = false
|
||||||
|
|
||||||
self.updateAnimations()
|
self.updateAnimations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func didLoad() {
|
||||||
|
super.didLoad()
|
||||||
|
|
||||||
|
self.view.addSubview(self.foregroundView)
|
||||||
|
self.foregroundView.layer.addSublayer(self.foregroundGradientLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layout() {
|
||||||
|
super.layout()
|
||||||
|
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
self.foregroundView.frame = self.bounds
|
||||||
|
self.foregroundGradientLayer.frame = self.bounds
|
||||||
|
self.maskCurveView.frame = self.bounds
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupGradientAnimations() {
|
||||||
|
return
|
||||||
|
if let _ = self.foregroundGradientLayer.animation(forKey: "movement") {
|
||||||
|
} else {
|
||||||
|
let previousValue = self.foregroundGradientLayer.startPoint
|
||||||
|
let newValue: CGPoint
|
||||||
|
if self.maskCurveView.presentationAudioLevel > 0.1 {
|
||||||
|
newValue = CGPoint(x: CGFloat.random(in: 1.0 ..< 1.3), y: 0.5)
|
||||||
|
} else {
|
||||||
|
newValue = CGPoint(x: CGFloat.random(in: 0.85 ..< 1.2), y: 0.5)
|
||||||
|
}
|
||||||
|
self.foregroundGradientLayer.startPoint = newValue
|
||||||
|
|
||||||
|
CATransaction.begin()
|
||||||
|
|
||||||
|
let animation = CABasicAnimation(keyPath: "endPoint")
|
||||||
|
animation.duration = Double.random(in: 0.8 ..< 1.4)
|
||||||
|
animation.fromValue = previousValue
|
||||||
|
animation.toValue = newValue
|
||||||
|
|
||||||
|
CATransaction.setCompletionBlock { [weak self] in
|
||||||
|
self?.setupGradientAnimations()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.foregroundGradientLayer.add(animation, forKey: "movement")
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func updateAnimations() {
|
func updateAnimations() {
|
||||||
self.presentationAudioLevel = self.presentationAudioLevel * 0.9 + max(0.1, CGFloat(self.audioLevel)) * 0.1
|
if !isCurrentlyInHierarchy {
|
||||||
for curve in self.curves {
|
self.foregroundGradientLayer.removeAllAnimations()
|
||||||
curve.level = self.presentationAudioLevel
|
self.maskCurveView.stopAnimating()
|
||||||
}
|
|
||||||
|
|
||||||
if self.gradientMovementArguments == nil {
|
|
||||||
self.gradientMovementArguments = (0.0, 0.7, CACurrentMediaTime(), 1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
let timestamp = CACurrentMediaTime()
|
|
||||||
if let (from, to, startTime, duration) = self.gradientMovementArguments, duration > 0.0 {
|
|
||||||
let progress = max(0.0, min(1.0, CGFloat((timestamp - startTime) / duration)))
|
|
||||||
self.gradientMovement = from + (to - from) * progress
|
|
||||||
if progress < 1.0 {
|
|
||||||
} else {
|
|
||||||
var nextTo: CGFloat
|
|
||||||
if to > 0.5 {
|
|
||||||
nextTo = CGFloat.random(in: 0.0 ..< 0.3)
|
|
||||||
} else {
|
|
||||||
if self.presentationAudioLevel > 0.3 {
|
|
||||||
nextTo = CGFloat.random(in: 0.75 ..< 1.0)
|
|
||||||
} else {
|
|
||||||
nextTo = CGFloat.random(in: 0.5 ..< 1.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.gradientMovementArguments = (to, nextTo, timestamp, Double.random(in: 0.8 ..< 1.5))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
for curve in self.curves {
|
|
||||||
curve.updateAnimations()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.setNeedsDisplay()
|
|
||||||
}
|
|
||||||
|
|
||||||
override var frame: CGRect {
|
|
||||||
didSet {
|
|
||||||
for curve in self.curves {
|
|
||||||
curve.size = self.frame.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
|
||||||
let timestamp = CACurrentMediaTime()
|
|
||||||
|
|
||||||
var gradientTransition: CGFloat = 0.0
|
|
||||||
gradientTransition = self.speaking ? 1.0 : 0.0
|
|
||||||
if let transition = self.transitionArguments {
|
|
||||||
gradientTransition = CGFloat((timestamp - transition.startTime) / transition.duration)
|
|
||||||
if !self.speaking {
|
|
||||||
gradientTransition = 1.0 - gradientTransition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var curves: [CurveDrawingState] = []
|
|
||||||
for curve in self.curves {
|
|
||||||
if let path = curve.currentShape?.copy() as? UIBezierPath {
|
|
||||||
curves.append(CurveDrawingState(path: path, offset: curve.currentOffset, alpha: curve.alpha))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return CallStatusBarBackgroundNodeDrawingState(timestamp: timestamp, curves: curves, speaking: self.speaking, gradientTransition: gradientTransition, gradientMovement: self.gradientMovement)
|
|
||||||
}
|
|
||||||
|
|
||||||
@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? CallStatusBarBackgroundNodeDrawingState else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
self.setupGradientAnimations()
|
||||||
context.interpolationQuality = .low
|
self.maskCurveView.startAnimating()
|
||||||
context.setBlendMode(.normal)
|
|
||||||
|
|
||||||
var locations: [CGFloat] = [0.0, 1.0]
|
|
||||||
let leftColor = UIColor(rgb: 0x007fff).interpolateTo(UIColor(rgb: 0x2bb76b), fraction: parameters.gradientTransition)!
|
|
||||||
let rightColor = UIColor(rgb: 0x00afff).interpolateTo(UIColor(rgb: 0x007fff), fraction: parameters.gradientTransition)!
|
|
||||||
let colors: [CGColor] = [leftColor.cgColor, rightColor.cgColor]
|
|
||||||
|
|
||||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
|
||||||
|
|
||||||
var i = 0
|
|
||||||
for curve in parameters.curves.reversed() {
|
|
||||||
context.saveGState()
|
|
||||||
|
|
||||||
let path = curve.path
|
|
||||||
if i < 2 {
|
|
||||||
let transform = CGAffineTransform(translationX: 0.0, y: min(1.0, curve.offset) * 16.0)
|
|
||||||
path.apply(transform)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.addPath(path.cgPath)
|
|
||||||
context.clip()
|
|
||||||
|
|
||||||
context.setAlpha(curve.alpha)
|
|
||||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: bounds.width + parameters.gradientMovement * bounds.width, y: 0.0), options: CGGradientDrawingOptions())
|
|
||||||
|
|
||||||
context.restoreGState()
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -511,12 +238,10 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var effectiveLevel: Float = 0.0
|
var effectiveLevel: Float = 0.0
|
||||||
let maxLevel: Float = 3.0
|
|
||||||
if !strongSelf.currentIsMuted {
|
if !strongSelf.currentIsMuted {
|
||||||
effectiveLevel = min(1.0, max(myAudioLevel / maxLevel, 0))
|
effectiveLevel = myAudioLevel
|
||||||
} else {
|
} else {
|
||||||
let level = audioLevels.map { $0.1 }.max() ?? 0.0
|
effectiveLevel = audioLevels.map { $0.1 }.max() ?? 0.0
|
||||||
effectiveLevel = min(1.0, max(level / maxLevel, 0))
|
|
||||||
}
|
}
|
||||||
strongSelf.backgroundNode.audioLevel = effectiveLevel
|
strongSelf.backgroundNode.audioLevel = effectiveLevel
|
||||||
}))
|
}))
|
||||||
@ -566,3 +291,356 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
|||||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 18.0))
|
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 18.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class VoiceCurveView: UIView {
|
||||||
|
private let smallCurve: CurveView
|
||||||
|
private let mediumCurve: CurveView
|
||||||
|
private let bigCurve: CurveView
|
||||||
|
|
||||||
|
private let maxLevel: CGFloat
|
||||||
|
|
||||||
|
private var displayLinkAnimator: ConstantDisplayLinkAnimator?
|
||||||
|
|
||||||
|
private var audioLevel: CGFloat = 0.0
|
||||||
|
var presentationAudioLevel: CGFloat = 0.0
|
||||||
|
|
||||||
|
private(set) var isAnimating = false
|
||||||
|
|
||||||
|
public typealias CurveRange = (min: CGFloat, max: CGFloat)
|
||||||
|
|
||||||
|
public init(
|
||||||
|
frame: CGRect,
|
||||||
|
maxLevel: CGFloat,
|
||||||
|
smallCurveRange: CurveRange,
|
||||||
|
mediumCurveRange: CurveRange,
|
||||||
|
bigCurveRange: CurveRange
|
||||||
|
) {
|
||||||
|
self.maxLevel = maxLevel
|
||||||
|
|
||||||
|
self.smallCurve = CurveView(
|
||||||
|
pointsCount: 7,
|
||||||
|
minRandomness: 1,
|
||||||
|
maxRandomness: 1.3,
|
||||||
|
minSpeed: 0.9,
|
||||||
|
maxSpeed: 3.5,
|
||||||
|
minOffset: smallCurveRange.min,
|
||||||
|
maxOffset: smallCurveRange.max
|
||||||
|
)
|
||||||
|
self.mediumCurve = CurveView(
|
||||||
|
pointsCount: 7,
|
||||||
|
minRandomness: 1.2,
|
||||||
|
maxRandomness: 1.5,
|
||||||
|
minSpeed: 1.0,
|
||||||
|
maxSpeed: 4.5,
|
||||||
|
minOffset: mediumCurveRange.min,
|
||||||
|
maxOffset: mediumCurveRange.max
|
||||||
|
)
|
||||||
|
self.bigCurve = CurveView(
|
||||||
|
pointsCount: 7,
|
||||||
|
minRandomness: 1.2,
|
||||||
|
maxRandomness: 1.7,
|
||||||
|
minSpeed: 1.0,
|
||||||
|
maxSpeed: 6.0,
|
||||||
|
minOffset: bigCurveRange.min,
|
||||||
|
maxOffset: bigCurveRange.max
|
||||||
|
)
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
addSubview(bigCurve)
|
||||||
|
addSubview(mediumCurve)
|
||||||
|
addSubview(smallCurve)
|
||||||
|
|
||||||
|
displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in
|
||||||
|
guard let strongSelf = self else { return }
|
||||||
|
|
||||||
|
strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1
|
||||||
|
|
||||||
|
strongSelf.smallCurve.level = strongSelf.presentationAudioLevel
|
||||||
|
strongSelf.mediumCurve.level = strongSelf.presentationAudioLevel
|
||||||
|
strongSelf.bigCurve.level = strongSelf.presentationAudioLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setColor(_ color: UIColor) {
|
||||||
|
smallCurve.setColor(color.withAlphaComponent(1.0))
|
||||||
|
mediumCurve.setColor(color.withAlphaComponent(0.55))
|
||||||
|
bigCurve.setColor(color.withAlphaComponent(0.35))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updateLevel(_ level: CGFloat) {
|
||||||
|
let normalizedLevel = min(1, max(level / maxLevel, 0))
|
||||||
|
|
||||||
|
smallCurve.updateSpeedLevel(to: normalizedLevel)
|
||||||
|
mediumCurve.updateSpeedLevel(to: normalizedLevel)
|
||||||
|
bigCurve.updateSpeedLevel(to: normalizedLevel)
|
||||||
|
|
||||||
|
audioLevel = normalizedLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
public func startAnimating() {
|
||||||
|
guard !isAnimating else { return }
|
||||||
|
isAnimating = true
|
||||||
|
|
||||||
|
updateCurvesState()
|
||||||
|
|
||||||
|
displayLinkAnimator?.isPaused = false
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stopAnimating() {
|
||||||
|
self.stopAnimating(duration: 0.15)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stopAnimating(duration: Double) {
|
||||||
|
guard isAnimating else { return }
|
||||||
|
isAnimating = false
|
||||||
|
|
||||||
|
updateCurvesState()
|
||||||
|
|
||||||
|
displayLinkAnimator?.isPaused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateCurvesState() {
|
||||||
|
if isAnimating {
|
||||||
|
if smallCurve.frame.size != .zero {
|
||||||
|
smallCurve.startAnimating()
|
||||||
|
mediumCurve.startAnimating()
|
||||||
|
bigCurve.startAnimating()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
smallCurve.stopAnimating()
|
||||||
|
mediumCurve.stopAnimating()
|
||||||
|
bigCurve.stopAnimating()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
smallCurve.frame = bounds
|
||||||
|
mediumCurve.frame = bounds
|
||||||
|
bigCurve.frame = bounds
|
||||||
|
|
||||||
|
updateCurvesState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class CurveView: UIView {
|
||||||
|
let pointsCount: Int
|
||||||
|
let smoothness: CGFloat
|
||||||
|
|
||||||
|
let minRandomness: CGFloat
|
||||||
|
let maxRandomness: CGFloat
|
||||||
|
|
||||||
|
let minSpeed: CGFloat
|
||||||
|
let maxSpeed: CGFloat
|
||||||
|
|
||||||
|
let minOffset: CGFloat
|
||||||
|
let maxOffset: CGFloat
|
||||||
|
|
||||||
|
var level: CGFloat = 0 {
|
||||||
|
didSet {
|
||||||
|
guard self.alpha == 1.0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
let lv = minOffset + (maxOffset - minOffset) * level
|
||||||
|
shapeLayer.transform = CATransform3DMakeTranslation(0.0, lv * 16.0, 0.0)
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var speedLevel: CGFloat = 0
|
||||||
|
private var lastSpeedLevel: CGFloat = 0
|
||||||
|
|
||||||
|
private let shapeLayer: CAShapeLayer = {
|
||||||
|
let layer = CAShapeLayer()
|
||||||
|
layer.strokeColor = nil
|
||||||
|
return layer
|
||||||
|
}()
|
||||||
|
|
||||||
|
private var transition: CGFloat = 0 {
|
||||||
|
didSet {
|
||||||
|
guard let currentPoints = currentPoints else { return }
|
||||||
|
|
||||||
|
shapeLayer.path = UIBezierPath.smoothCurve(through: currentPoints, length: bounds.width, smoothness: smoothness, curve: true).cgPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var frame: CGRect {
|
||||||
|
didSet {
|
||||||
|
if self.frame != oldValue {
|
||||||
|
self.fromPoints = nil
|
||||||
|
self.toPoints = nil
|
||||||
|
self.animateToNewShape()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var fromPoints: [CGPoint]?
|
||||||
|
private var toPoints: [CGPoint]?
|
||||||
|
|
||||||
|
private var currentPoints: [CGPoint]? {
|
||||||
|
guard let fromPoints = fromPoints, let toPoints = toPoints else { return nil }
|
||||||
|
|
||||||
|
return fromPoints.enumerated().map { offset, fromPoint in
|
||||||
|
let toPoint = toPoints[offset]
|
||||||
|
return CGPoint(
|
||||||
|
x: fromPoint.x + (toPoint.x - fromPoint.x) * transition,
|
||||||
|
y: fromPoint.y + (toPoint.y - fromPoint.y) * transition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
pointsCount: Int,
|
||||||
|
minRandomness: CGFloat,
|
||||||
|
maxRandomness: CGFloat,
|
||||||
|
minSpeed: CGFloat,
|
||||||
|
maxSpeed: CGFloat,
|
||||||
|
minOffset: CGFloat,
|
||||||
|
maxOffset: CGFloat
|
||||||
|
) {
|
||||||
|
self.pointsCount = pointsCount
|
||||||
|
self.minRandomness = minRandomness
|
||||||
|
self.maxRandomness = maxRandomness
|
||||||
|
self.minSpeed = minSpeed
|
||||||
|
self.maxSpeed = maxSpeed
|
||||||
|
self.minOffset = minOffset
|
||||||
|
self.maxOffset = maxOffset
|
||||||
|
|
||||||
|
self.smoothness = 0.35
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
layer.addSublayer(shapeLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setColor(_ color: UIColor) {
|
||||||
|
shapeLayer.fillColor = color.cgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSpeedLevel(to newSpeedLevel: CGFloat) {
|
||||||
|
speedLevel = max(speedLevel, newSpeedLevel)
|
||||||
|
|
||||||
|
if abs(lastSpeedLevel - newSpeedLevel) > 0.3 {
|
||||||
|
animateToNewShape()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startAnimating() {
|
||||||
|
animateToNewShape()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopAnimating() {
|
||||||
|
fromPoints = currentPoints
|
||||||
|
toPoints = nil
|
||||||
|
pop_removeAnimation(forKey: "curve")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func animateToNewShape() {
|
||||||
|
if pop_animation(forKey: "curve") != nil {
|
||||||
|
fromPoints = currentPoints
|
||||||
|
toPoints = nil
|
||||||
|
pop_removeAnimation(forKey: "curve")
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromPoints == nil {
|
||||||
|
fromPoints = generateNextCurve(for: bounds.size)
|
||||||
|
}
|
||||||
|
if toPoints == nil {
|
||||||
|
toPoints = generateNextCurve(for: bounds.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
let animation = POPBasicAnimation()
|
||||||
|
animation.property = POPAnimatableProperty.property(withName: "curve.transition", initializer: { property in
|
||||||
|
property?.readBlock = { curveView, values in
|
||||||
|
guard let curveView = curveView as? CurveView, let values = values else { return }
|
||||||
|
|
||||||
|
values.pointee = curveView.transition
|
||||||
|
}
|
||||||
|
property?.writeBlock = { curveView, values in
|
||||||
|
guard let curveView = curveView as? CurveView, let values = values else { return }
|
||||||
|
|
||||||
|
curveView.transition = values.pointee
|
||||||
|
}
|
||||||
|
}) as? POPAnimatableProperty
|
||||||
|
animation.completionBlock = { [weak self] animation, finished in
|
||||||
|
if finished {
|
||||||
|
self?.fromPoints = self?.currentPoints
|
||||||
|
self?.toPoints = nil
|
||||||
|
self?.animateToNewShape()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animation.duration = CFTimeInterval(1 / (minSpeed + (maxSpeed - minSpeed) * speedLevel))
|
||||||
|
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||||
|
animation.fromValue = 0
|
||||||
|
animation.toValue = 1
|
||||||
|
pop_add(animation, forKey: "curve")
|
||||||
|
|
||||||
|
lastSpeedLevel = speedLevel
|
||||||
|
speedLevel = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateNextCurve(for size: CGSize) -> [CGPoint] {
|
||||||
|
let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel
|
||||||
|
return curve(pointsCount: pointsCount, randomness: randomness).map {
|
||||||
|
return CGPoint(x: $0.x * CGFloat(size.width), y: size.height - 18.0 + $0.y * 12.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func curve(pointsCount: Int, randomness: CGFloat) -> [CGPoint] {
|
||||||
|
let segment = 1.0 / CGFloat(pointsCount - 1)
|
||||||
|
|
||||||
|
let rgen = { () -> CGFloat in
|
||||||
|
let accuracy: UInt32 = 1000
|
||||||
|
let random = arc4random_uniform(accuracy)
|
||||||
|
return CGFloat(random) / CGFloat(accuracy)
|
||||||
|
}
|
||||||
|
let rangeStart: CGFloat = 1.0 / (1.0 + randomness / 10.0)
|
||||||
|
|
||||||
|
let points = (0 ..< pointsCount).map { i -> CGPoint in
|
||||||
|
let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2
|
||||||
|
let segmentRandomness: CGFloat = randomness
|
||||||
|
|
||||||
|
let pointX: CGFloat
|
||||||
|
let pointY: CGFloat
|
||||||
|
let randomXDelta: CGFloat
|
||||||
|
if i == 0 {
|
||||||
|
pointX = 0.0
|
||||||
|
pointY = 0.0
|
||||||
|
randomXDelta = 0.0
|
||||||
|
} else if i == pointsCount - 1 {
|
||||||
|
pointX = 1.0
|
||||||
|
pointY = 0.0
|
||||||
|
randomXDelta = 0.0
|
||||||
|
} else {
|
||||||
|
pointX = segment * CGFloat(i)
|
||||||
|
pointY = ((segmentRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - segmentRandomness * 0.5) * randPointOffset
|
||||||
|
randomXDelta = segment - segment * randPointOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
return CGPoint(x: pointX + randomXDelta, y: pointY)
|
||||||
|
}
|
||||||
|
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
shapeLayer.frame = self.bounds
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -143,7 +143,6 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
|
|
||||||
self.contentNode.addSubnode(self.titleNode)
|
self.contentNode.addSubnode(self.titleNode)
|
||||||
self.contentNode.addSubnode(self.textNode)
|
self.contentNode.addSubnode(self.textNode)
|
||||||
self.contentNode.addSubnode(self.muteIconNode)
|
|
||||||
|
|
||||||
self.contentNode.addSubnode(self.avatarsNode)
|
self.contentNode.addSubnode(self.avatarsNode)
|
||||||
|
|
||||||
@ -401,7 +400,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
|
|
||||||
if let avatarsContent = self.avatarsContent {
|
if let avatarsContent = self.avatarsContent {
|
||||||
let avatarsSize = self.avatarsNode.update(context: self.context, content: avatarsContent, itemSize: CGSize(width: 32.0, height: 32.0), animated: true, synchronousLoad: true)
|
let avatarsSize = self.avatarsNode.update(context: self.context, content: avatarsContent, itemSize: CGSize(width: 32.0, height: 32.0), animated: true, synchronousLoad: true)
|
||||||
transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: 7.0, y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize))
|
transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarsSize.width) / 2.0), y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
let joinButtonTitleSize = self.joinButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude))
|
let joinButtonTitleSize = self.joinButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude))
|
||||||
@ -442,9 +441,9 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
|
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
|
||||||
let textSize = self.textNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
|
let textSize = self.textNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
|
||||||
|
|
||||||
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: 9.0), size: titleSize)
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: 9.0), size: titleSize)
|
||||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: textSize))
|
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset + 16.0, y: titleFrame.maxY + 1.0), size: textSize))
|
||||||
|
|
||||||
if let image = self.muteIconNode.image {
|
if let image = self.muteIconNode.image {
|
||||||
transition.updateFrame(node: self.muteIconNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + 5.0), size: image.size))
|
transition.updateFrame(node: self.muteIconNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + 5.0), size: image.size))
|
||||||
|
@ -195,15 +195,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
return self.audioOutputStatePromise.get()
|
return self.audioOutputStatePromise.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
private let audioLevelsPipe = ValuePipe<[(PeerId, Float)]>()
|
|
||||||
public var audioLevels: Signal<[(PeerId, Float)], NoError> {
|
|
||||||
return self.audioLevelsPipe.signal()
|
|
||||||
}
|
|
||||||
private var audioLevelsDisposable = MetaDisposable()
|
private var audioLevelsDisposable = MetaDisposable()
|
||||||
|
|
||||||
private let speakingParticipantsContext = SpeakingParticipantsContext()
|
private let speakingParticipantsContext = SpeakingParticipantsContext()
|
||||||
private var speakingParticipantsReportTimestamp: [PeerId: Double] = [:]
|
private var speakingParticipantsReportTimestamp: [PeerId: Double] = [:]
|
||||||
public var speakingAudioLevels: Signal<[(PeerId, Float)], NoError> {
|
public var audioLevels: Signal<[(PeerId, Float)], NoError> {
|
||||||
return self.speakingParticipantsContext.getAudioLevels()
|
return self.speakingParticipantsContext.getAudioLevels()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -569,9 +565,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
result.append((peerId, level))
|
result.append((peerId, level))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !result.isEmpty {
|
|
||||||
strongSelf.audioLevelsPipe.putNext(result)
|
|
||||||
}
|
|
||||||
strongSelf.speakingParticipantsContext.update(levels: result)
|
strongSelf.speakingParticipantsContext.update(levels: result)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -585,6 +578,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
|
|
||||||
strongSelf.myAudioLevelPipe.putNext(mappedLevel)
|
strongSelf.myAudioLevelPipe.putNext(mappedLevel)
|
||||||
strongSelf.processMyAudioLevel(level: mappedLevel)
|
strongSelf.processMyAudioLevel(level: mappedLevel)
|
||||||
|
if !strongSelf.isMutedValue.isEffectivelyMuted {
|
||||||
|
strongSelf.speakingParticipantsContext.update(levels: [(strongSelf.account.peerId, mappedLevel)])
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,14 +96,11 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateLevel(_ level: CGFloat) {
|
func updateLevel(_ level: CGFloat) {
|
||||||
let maxLevel: CGFloat = 6.0
|
self.backgroundNode.audioLevel = level
|
||||||
let normalizedLevel = min(1, max(level / maxLevel, 0))
|
|
||||||
|
|
||||||
self.backgroundNode.audioLevel = normalizedLevel
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyParams(animated: Bool) {
|
func applyParams(animated: Bool) {
|
||||||
guard let (size, _, state, simplified, title, subtitle) = self.currentParams else {
|
guard let (size, _, _, simplified, title, subtitle) = self.currentParams else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,7 +346,7 @@ private final class VoiceChatActionButtonBackgroundNewNode: ASDisplayNode {
|
|||||||
let whiteColor = UIColor(rgb: 0xffffff)
|
let whiteColor = UIColor(rgb: 0xffffff)
|
||||||
|
|
||||||
let blobSize = CGSize(width: 244.0, height: 244.0)
|
let blobSize = CGSize(width: 244.0, height: 244.0)
|
||||||
self.maskBlobView = VoiceBlobView(frame: CGRect(origin: CGPoint(x: (300.0 - blobSize.width) / 2.0, y: (300.0 - blobSize.height) / 2.0), size: blobSize), maxLevel: 4.0, mediumBlobRange: (0.69, 0.87), bigBlobRange: (0.71, 1.0))
|
self.maskBlobView = VoiceBlobView(frame: CGRect(origin: CGPoint(x: (300.0 - blobSize.width) / 2.0, y: (300.0 - blobSize.height) / 2.0), size: blobSize), maxLevel: 2.5, mediumBlobRange: (0.69, 0.87), bigBlobRange: (0.71, 1.0))
|
||||||
self.maskBlobView.setColor(UIColor(rgb: 0xffffff))
|
self.maskBlobView.setColor(UIColor(rgb: 0xffffff))
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
@ -432,8 +429,10 @@ private final class VoiceChatActionButtonBackgroundNewNode: ASDisplayNode {
|
|||||||
animation.toValue = newValue
|
animation.toValue = newValue
|
||||||
|
|
||||||
CATransaction.setCompletionBlock { [weak self] in
|
CATransaction.setCompletionBlock { [weak self] in
|
||||||
|
if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy {
|
||||||
self?.setupGradientAnimations()
|
self?.setupGradientAnimations()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.foregroundGradientLayer.add(animation, forKey: "movement")
|
self.foregroundGradientLayer.add(animation, forKey: "movement")
|
||||||
CATransaction.commit()
|
CATransaction.commit()
|
||||||
@ -585,6 +584,13 @@ private final class VoiceChatActionButtonBackgroundNewNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateAnimations() {
|
func updateAnimations() {
|
||||||
|
if !self.isCurrentlyInHierarchy {
|
||||||
|
self.foregroundGradientLayer.removeAllAnimations()
|
||||||
|
self.maskGradientLayer.removeAllAnimations()
|
||||||
|
self.maskProgressLayer.removeAllAnimations()
|
||||||
|
self.maskBlobView.stopAnimating()
|
||||||
|
return
|
||||||
|
}
|
||||||
self.setupGradientAnimations()
|
self.setupGradientAnimations()
|
||||||
|
|
||||||
switch self.state {
|
switch self.state {
|
||||||
@ -648,7 +654,7 @@ private final class VoiceChatActionButtonBackgroundNewNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration {
|
private final class VoiceBlobView: UIView {
|
||||||
private let mediumBlob: BlobView
|
private let mediumBlob: BlobView
|
||||||
private let bigBlob: BlobView
|
private let bigBlob: BlobView
|
||||||
|
|
||||||
@ -678,9 +684,7 @@ private final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDec
|
|||||||
minSpeed: 0.85,
|
minSpeed: 0.85,
|
||||||
maxSpeed: 7,
|
maxSpeed: 7,
|
||||||
minScale: mediumBlobRange.min,
|
minScale: mediumBlobRange.min,
|
||||||
maxScale: mediumBlobRange.max,
|
maxScale: mediumBlobRange.max
|
||||||
scaleSpeed: 0.2,
|
|
||||||
isCircle: false
|
|
||||||
)
|
)
|
||||||
self.bigBlob = BlobView(
|
self.bigBlob = BlobView(
|
||||||
pointsCount: 8,
|
pointsCount: 8,
|
||||||
@ -689,9 +693,7 @@ private final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDec
|
|||||||
minSpeed: 0.85,
|
minSpeed: 0.85,
|
||||||
maxSpeed: 7,
|
maxSpeed: 7,
|
||||||
minScale: bigBlobRange.min,
|
minScale: bigBlobRange.min,
|
||||||
maxScale: bigBlobRange.max,
|
maxScale: bigBlobRange.max
|
||||||
scaleSpeed: 0.2,
|
|
||||||
isCircle: false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
@ -783,11 +785,6 @@ final class BlobView: UIView {
|
|||||||
|
|
||||||
let minScale: CGFloat
|
let minScale: CGFloat
|
||||||
let maxScale: CGFloat
|
let maxScale: CGFloat
|
||||||
let scaleSpeed: CGFloat
|
|
||||||
|
|
||||||
var scaleLevelsToBalance = [CGFloat]()
|
|
||||||
|
|
||||||
let isCircle: Bool
|
|
||||||
|
|
||||||
var level: CGFloat = 0 {
|
var level: CGFloat = 0 {
|
||||||
didSet {
|
didSet {
|
||||||
@ -800,10 +797,7 @@ final class BlobView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var speedLevel: CGFloat = 0
|
private var speedLevel: CGFloat = 0
|
||||||
private var scaleLevel: CGFloat = 0
|
|
||||||
|
|
||||||
private var lastSpeedLevel: CGFloat = 0
|
private var lastSpeedLevel: CGFloat = 0
|
||||||
private var lastScaleLevel: CGFloat = 0
|
|
||||||
|
|
||||||
private let shapeLayer: CAShapeLayer = {
|
private let shapeLayer: CAShapeLayer = {
|
||||||
let layer = CAShapeLayer()
|
let layer = CAShapeLayer()
|
||||||
@ -841,9 +835,7 @@ final class BlobView: UIView {
|
|||||||
minSpeed: CGFloat,
|
minSpeed: CGFloat,
|
||||||
maxSpeed: CGFloat,
|
maxSpeed: CGFloat,
|
||||||
minScale: CGFloat,
|
minScale: CGFloat,
|
||||||
maxScale: CGFloat,
|
maxScale: CGFloat
|
||||||
scaleSpeed: CGFloat,
|
|
||||||
isCircle: Bool
|
|
||||||
) {
|
) {
|
||||||
self.pointsCount = pointsCount
|
self.pointsCount = pointsCount
|
||||||
self.minRandomness = minRandomness
|
self.minRandomness = minRandomness
|
||||||
@ -852,8 +844,6 @@ final class BlobView: UIView {
|
|||||||
self.maxSpeed = maxSpeed
|
self.maxSpeed = maxSpeed
|
||||||
self.minScale = minScale
|
self.minScale = minScale
|
||||||
self.maxScale = maxScale
|
self.maxScale = maxScale
|
||||||
self.scaleSpeed = scaleSpeed
|
|
||||||
self.isCircle = isCircle
|
|
||||||
|
|
||||||
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
|
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
|
||||||
self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2
|
self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2
|
||||||
@ -876,7 +866,7 @@ final class BlobView: UIView {
|
|||||||
func updateSpeedLevel(to newSpeedLevel: CGFloat) {
|
func updateSpeedLevel(to newSpeedLevel: CGFloat) {
|
||||||
speedLevel = max(speedLevel, newSpeedLevel)
|
speedLevel = max(speedLevel, newSpeedLevel)
|
||||||
|
|
||||||
if abs(lastSpeedLevel - newSpeedLevel) > 0.5 {
|
if abs(lastSpeedLevel - newSpeedLevel) > 0.3 {
|
||||||
animateToNewShape()
|
animateToNewShape()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -892,8 +882,6 @@ final class BlobView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func animateToNewShape() {
|
private func animateToNewShape() {
|
||||||
guard !isCircle else { return }
|
|
||||||
|
|
||||||
if pop_animation(forKey: "blob") != nil {
|
if pop_animation(forKey: "blob") != nil {
|
||||||
fromPoints = currentPoints
|
fromPoints = currentPoints
|
||||||
toPoints = nil
|
toPoints = nil
|
||||||
@ -983,90 +971,6 @@ final class BlobView: UIView {
|
|||||||
CATransaction.begin()
|
CATransaction.begin()
|
||||||
CATransaction.setDisableActions(true)
|
CATransaction.setDisableActions(true)
|
||||||
shapeLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
|
shapeLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||||
if isCircle {
|
|
||||||
let halfWidth = bounds.width * 0.5
|
|
||||||
shapeLayer.path = UIBezierPath(
|
|
||||||
roundedRect: bounds.offsetBy(dx: -halfWidth, dy: -halfWidth),
|
|
||||||
cornerRadius: halfWidth
|
|
||||||
).cgPath
|
|
||||||
}
|
|
||||||
CATransaction.commit()
|
CATransaction.commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|
||||||
// @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
|
||||||
// let context = UIGraphicsGetCurrentContext()!
|
|
||||||
//
|
|
||||||
// guard let parameters = parameters as? VoiceChatActionButtonBackgroundNodeDrawingState else {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// context.setBlendMode(.normal)
|
|
||||||
//
|
|
||||||
// let buttonSize = CGSize(width: 144.0, height: 144.0)
|
|
||||||
// let radius = buttonSize.width / 2.0
|
|
||||||
//
|
|
||||||
// var gradientCenter = CGPoint(x: bounds.size.width, y: 50.0)
|
|
||||||
// gradientCenter.x -= 90.0 * parameters.gradientMovement.x
|
|
||||||
// gradientCenter.y += 120.0 * parameters.gradientMovement.y
|
|
||||||
//
|
|
||||||
// var gradientTransition: CGFloat = 0.0
|
|
||||||
// var simpleColor: UIColor = blue
|
|
||||||
// var firstColor = lightBlue
|
|
||||||
// var secondColor = blue
|
|
||||||
//
|
|
||||||
// context.interpolationQuality = .low
|
|
||||||
//
|
|
||||||
// var appearanceProgress: CGFloat = 1.0
|
|
||||||
// var glowScale: CGFloat = 0.75
|
|
||||||
// if let transition = parameters.transition, transition.previousState == .connecting || transition.previousState == .disabled {
|
|
||||||
// appearanceProgress = transition.transition
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// parameters.maskContext.with { maskContext in
|
|
||||||
// maskContext.clear(bounds)
|
|
||||||
//
|
|
||||||
// var skipBlobs = false
|
|
||||||
// if parameters.state is VoiceChatActionButtonBackgroundNodeBlobState, let transition = parameters.transition, transition.previousState == .connecting, transition.transition < 0.5 {
|
|
||||||
// skipBlobs = true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// var drawGradient = false
|
|
||||||
// if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState, !skipBlobs {
|
|
||||||
// gradientTransition = blobsState.active ? 1.0 : 0.0
|
|
||||||
// if let transition = blobsState.activeTransitionArguments {
|
|
||||||
// gradientTransition = CGFloat((parameters.timestamp - transition.startTime) / transition.duration)
|
|
||||||
// if !blobsState.active {
|
|
||||||
// gradientTransition = 1.0 - gradientTransition
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// glowScale += gradientTransition * 0.3
|
|
||||||
//
|
|
||||||
// simpleColor = blue.interpolateTo(green, fraction: gradientTransition)!
|
|
||||||
// firstColor = firstColor.interpolateTo(blue, fraction: gradientTransition)!
|
|
||||||
// secondColor = secondColor.interpolateTo(green, fraction: gradientTransition)!
|
|
||||||
//
|
|
||||||
// let progress = 1.0 - (appearanceProgress * glowScale)
|
|
||||||
// let maskBounds = bounds.insetBy(dx: bounds.width / 3.0 * progress, dy: bounds.width / 3.0 * progress)
|
|
||||||
// if let radialMask = radialMaskImage.cgImage {
|
|
||||||
// maskContext.setBlendMode(.copy)
|
|
||||||
// maskContext.draw(radialMask, in: maskBounds)
|
|
||||||
// maskContext.setBlendMode(.normal)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// for blob in blobsState.blobs {
|
|
||||||
// maskContext.addPath(blob.path)
|
|
||||||
// maskContext.setFillColor(UIColor(rgb: 0xffffff, alpha: blob.alpha).cgColor)
|
|
||||||
// maskContext.fillPath()
|
|
||||||
// }
|
|
||||||
// drawGradient = true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
@ -144,27 +144,21 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAudioLevels(_ levels: [(PeerId, Float)], accountPeerId: PeerId, updateAll: Bool) {
|
func updateAudioLevels(_ levels: [(PeerId, Float)]) {
|
||||||
var updated = Set<PeerId>()
|
var updated = Set<PeerId>()
|
||||||
for (peerId, level) in levels {
|
for (peerId, level) in levels {
|
||||||
if let pipe = self.audioLevels[peerId] {
|
if let pipe = self.audioLevels[peerId] {
|
||||||
var level = level
|
pipe.putNext(max(0.001, level))
|
||||||
if peerId != accountPeerId {
|
|
||||||
level = max(0.001, level)
|
|
||||||
}
|
|
||||||
pipe.putNext(level)
|
|
||||||
updated.insert(peerId)
|
updated.insert(peerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if updateAll {
|
|
||||||
for (peerId, pipe) in self.audioLevels {
|
for (peerId, pipe) in self.audioLevels {
|
||||||
if !updated.contains(peerId) && peerId != accountPeerId {
|
if !updated.contains(peerId) {
|
||||||
pipe.putNext(0.0)
|
pipe.putNext(0.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private struct PeerEntry: Comparable, Identifiable {
|
private struct PeerEntry: Comparable, Identifiable {
|
||||||
enum State {
|
enum State {
|
||||||
@ -634,12 +628,12 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
self.audioLevelsDisposable = (call.speakingAudioLevels
|
self.audioLevelsDisposable = (call.audioLevels
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] levels in
|
|> deliverOnMainQueue).start(next: { [weak self] levels in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.itemInteraction?.updateAudioLevels(levels, accountPeerId: strongSelf.context.account.peerId, updateAll: true)
|
strongSelf.itemInteraction?.updateAudioLevels(levels)
|
||||||
})
|
})
|
||||||
|
|
||||||
self.myAudioLevelDisposable = (call.myAudioLevel
|
self.myAudioLevelDisposable = (call.myAudioLevel
|
||||||
@ -651,7 +645,6 @@ public final class VoiceChatController: ViewController {
|
|||||||
if let state = strongSelf.callState, state.muteState == nil || strongSelf.pushingToTalk {
|
if let state = strongSelf.callState, state.muteState == nil || strongSelf.pushingToTalk {
|
||||||
effectiveLevel = level
|
effectiveLevel = level
|
||||||
}
|
}
|
||||||
strongSelf.itemInteraction?.updateAudioLevels([(strongSelf.context.account.peerId, effectiveLevel)], accountPeerId: strongSelf.context.account.peerId, updateAll: false)
|
|
||||||
strongSelf.actionButton.updateLevel(CGFloat(effectiveLevel))
|
strongSelf.actionButton.updateLevel(CGFloat(effectiveLevel))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -789,6 +782,8 @@ public final class VoiceChatController: ViewController {
|
|||||||
override func didLoad() {
|
override func didLoad() {
|
||||||
super.didLoad()
|
super.didLoad()
|
||||||
|
|
||||||
|
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||||
|
|
||||||
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.actionButtonPressGesture(_:)))
|
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.actionButtonPressGesture(_:)))
|
||||||
longTapRecognizer.minimumPressDuration = 0.001
|
longTapRecognizer.minimumPressDuration = 0.001
|
||||||
longTapRecognizer.delegate = self
|
longTapRecognizer.delegate = self
|
||||||
@ -815,7 +810,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func leavePressed() {
|
@objc private func leavePressed() {
|
||||||
self.hapticFeedback.impact(.veryLight)
|
self.hapticFeedback.impact(.light)
|
||||||
|
|
||||||
self.leaveDisposable.set((self.call.leave(terminateIfPossible: false)
|
self.leaveDisposable.set((self.call.leave(terminateIfPossible: false)
|
||||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||||
@ -823,6 +818,12 @@ public final class VoiceChatController: ViewController {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
if case .ended = recognizer.state {
|
||||||
|
self.controller?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var actionButtonPressGestureStartTime: Double = 0.0
|
private var actionButtonPressGestureStartTime: Double = 0.0
|
||||||
|
|
||||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
@ -844,7 +845,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
switch gestureRecognizer.state {
|
switch gestureRecognizer.state {
|
||||||
case .began:
|
case .began:
|
||||||
self.hapticFeedback.impact(.veryLight)
|
self.hapticFeedback.impact(.light)
|
||||||
|
|
||||||
self.actionButtonPressGestureStartTime = CACurrentMediaTime()
|
self.actionButtonPressGestureStartTime = CACurrentMediaTime()
|
||||||
self.actionButton.pressing = true
|
self.actionButton.pressing = true
|
||||||
@ -857,7 +858,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
self.updateMembers(muteState: self.effectiveMuteState, groupMembers: self.currentGroupMembers ?? [], callMembers: self.currentCallMembers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set())
|
self.updateMembers(muteState: self.effectiveMuteState, groupMembers: self.currentGroupMembers ?? [], callMembers: self.currentCallMembers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set())
|
||||||
case .ended, .cancelled:
|
case .ended, .cancelled:
|
||||||
self.hapticFeedback.impact(.veryLight)
|
self.hapticFeedback.impact(.light)
|
||||||
|
|
||||||
self.pushingToTalk = false
|
self.pushingToTalk = false
|
||||||
self.actionButton.pressing = false
|
self.actionButton.pressing = false
|
||||||
@ -887,7 +888,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func audioOutputPressed() {
|
@objc private func audioOutputPressed() {
|
||||||
self.hapticFeedback.impact(.veryLight)
|
self.hapticFeedback.impact(.light)
|
||||||
|
|
||||||
guard let (availableOutputs, currentOutput) = self.audioOutputState else {
|
guard let (availableOutputs, currentOutput) = self.audioOutputState else {
|
||||||
return
|
return
|
||||||
@ -1067,7 +1068,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
soundImage = .speaker
|
soundImage = .speaker
|
||||||
case .speaker:
|
case .speaker:
|
||||||
soundImage = .speaker
|
soundImage = .speaker
|
||||||
// soundAppearance = .blurred(isFilled: true)
|
soundAppearance = .blurred(isFilled: true)
|
||||||
case .headphones:
|
case .headphones:
|
||||||
soundImage = .bluetooth
|
soundImage = .bluetooth
|
||||||
case let .bluetooth(type):
|
case let .bluetooth(type):
|
||||||
@ -1228,7 +1229,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
var memberMuteState: GroupCallParticipantsContext.Participant.MuteState?
|
var memberMuteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||||
if member.peer.id == self.context.account.peerId {
|
if member.peer.id == self.context.account.peerId {
|
||||||
if muteState == nil {
|
if muteState == nil {
|
||||||
memberState = .speaking
|
memberState = speakingPeers.contains(member.peer.id) ? .speaking : .listening
|
||||||
} else {
|
} else {
|
||||||
memberState = .listening
|
memberState = .listening
|
||||||
memberMuteState = member.muteState
|
memberMuteState = member.muteState
|
||||||
|
Loading…
x
Reference in New Issue
Block a user