diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 75e5db62f4..93491b67ab 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -230,6 +230,15 @@ public struct PresentationGroupCallMemberState: Equatable { public enum PresentationGroupCallMuteAction: Equatable { case muted(isPushToTalkActive: Bool) case unmuted + + public var isEffectivelyMuted: Bool { + switch self { + case let .muted(isPushToTalkActive): + return !isPushToTalkActive + case .unmuted: + return false + } + } } public struct PresentationGroupCallMembers: Equatable { @@ -265,7 +274,6 @@ public protocol PresentationGroupCall: class { var members: Signal { get } var audioLevels: Signal<[(PeerId, Float)], NoError> { get } var myAudioLevel: Signal { get } - var speakingAudioLevels: Signal<[(PeerId, Float)], NoError> { get } var isMuted: Signal { get } func leave(terminateIfPossible: Bool) -> Signal diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 6047e8e915..0d25c3e4e3 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -378,7 +378,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { return } - let panelData = currentState ?? availableState + let panelData = currentState != nil ? nil : availableState let wasEmpty = strongSelf.groupCallPanelData == nil strongSelf.groupCallPanelData = panelData diff --git a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift index 3eaa2f76c9..6f3fce2ab9 100644 --- a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift @@ -8,401 +8,128 @@ import TelegramCore import TelegramPresentationData import TelegramUIPreferences import AccountContext +import LegacyComponents -private let colorSpace = CGColorSpaceCreateDeviceRGB() - -private class CurveDrawingState: NSObject { - 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 let blue = UIColor(rgb: 0x0078ff) +private let lightBlue = UIColor(rgb: 0x59c7f8) +private let green = UIColor(rgb: 0x33c659) 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 { - for curve in self.curves { - curve.loop = audioLevel.isZero - curve.updateSpeedLevel(to: CGFloat(self.audioLevel)) - } + self.maskCurveView.updateLevel(CGFloat(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 { didSet { 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() { - let smallCurveRange: CurveRange = (0.0, 0.0) - let mediumCurveRange: CurveRange = (0.1, 0.55) - let bigCurveRange: CurveRange = (0.1, 1.0) - - 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] + self.foregroundView = UIView() + self.foregroundGradientLayer = CAGradientLayer() + 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)) 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.updateAnimations() } - func updateAnimations() { - self.presentationAudioLevel = self.presentationAudioLevel * 0.9 + max(0.1, CGFloat(self.audioLevel)) * 0.1 - for curve in self.curves { - curve.level = self.presentationAudioLevel - } + override func didLoad() { + super.didLoad() - if self.gradientMovementArguments == nil { - self.gradientMovementArguments = (0.0, 0.7, CACurrentMediaTime(), 1.0) - } + self.view.addSubview(self.foregroundView) + self.foregroundView.layer.addSublayer(self.foregroundGradientLayer) + } + + override func layout() { + super.layout() - 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 + 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 { - 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 + 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() } } - 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 { + func updateAnimations() { + if !isCurrentlyInHierarchy { + self.foregroundGradientLayer.removeAllAnimations() + self.maskCurveView.stopAnimating() return } - - context.interpolationQuality = .low - 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 - } + self.setupGradientAnimations() + self.maskCurveView.startAnimating() } } @@ -511,12 +238,10 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { return } var effectiveLevel: Float = 0.0 - let maxLevel: Float = 3.0 if !strongSelf.currentIsMuted { - effectiveLevel = min(1.0, max(myAudioLevel / maxLevel, 0)) + effectiveLevel = myAudioLevel } else { - let level = audioLevels.map { $0.1 }.max() ?? 0.0 - effectiveLevel = min(1.0, max(level / maxLevel, 0)) + effectiveLevel = audioLevels.map { $0.1 }.max() ?? 0.0 } 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)) } } + +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() + } +} diff --git a/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift b/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift index f1f78ec91d..346dc68745 100644 --- a/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift +++ b/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift @@ -143,7 +143,6 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { self.contentNode.addSubnode(self.titleNode) self.contentNode.addSubnode(self.textNode) - self.contentNode.addSubnode(self.muteIconNode) self.contentNode.addSubnode(self.avatarsNode) @@ -401,7 +400,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { 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) - 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)) @@ -442,9 +441,9 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { let titleSize = self.titleNode.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.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 { transition.updateFrame(node: self.muteIconNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + 5.0), size: image.size)) diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index ff3d6f31f7..fb3dc68fdd 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -195,15 +195,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { 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 let speakingParticipantsContext = SpeakingParticipantsContext() private var speakingParticipantsReportTimestamp: [PeerId: Double] = [:] - public var speakingAudioLevels: Signal<[(PeerId, Float)], NoError> { + public var audioLevels: Signal<[(PeerId, Float)], NoError> { return self.speakingParticipantsContext.getAudioLevels() } @@ -569,9 +565,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { result.append((peerId, level)) } } - if !result.isEmpty { - strongSelf.audioLevelsPipe.putNext(result) - } strongSelf.speakingParticipantsContext.update(levels: result) })) @@ -585,6 +578,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { strongSelf.myAudioLevelPipe.putNext(mappedLevel) strongSelf.processMyAudioLevel(level: mappedLevel) + if !strongSelf.isMutedValue.isEffectivelyMuted { + strongSelf.speakingParticipantsContext.update(levels: [(strongSelf.account.peerId, mappedLevel)]) + } })) } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift index df7c025bf5..828569d99a 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift @@ -96,14 +96,11 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { } func updateLevel(_ level: CGFloat) { - let maxLevel: CGFloat = 6.0 - let normalizedLevel = min(1, max(level / maxLevel, 0)) - - self.backgroundNode.audioLevel = normalizedLevel + self.backgroundNode.audioLevel = level } func applyParams(animated: Bool) { - guard let (size, _, state, simplified, title, subtitle) = self.currentParams else { + guard let (size, _, _, simplified, title, subtitle) = self.currentParams else { return } @@ -349,7 +346,7 @@ private final class VoiceChatActionButtonBackgroundNewNode: ASDisplayNode { let whiteColor = UIColor(rgb: 0xffffff) 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)) super.init() @@ -432,7 +429,9 @@ private final class VoiceChatActionButtonBackgroundNewNode: ASDisplayNode { animation.toValue = newValue CATransaction.setCompletionBlock { [weak self] in - self?.setupGradientAnimations() + if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy { + self?.setupGradientAnimations() + } } self.foregroundGradientLayer.add(animation, forKey: "movement") @@ -585,6 +584,13 @@ private final class VoiceChatActionButtonBackgroundNewNode: ASDisplayNode { } func updateAnimations() { + if !self.isCurrentlyInHierarchy { + self.foregroundGradientLayer.removeAllAnimations() + self.maskGradientLayer.removeAllAnimations() + self.maskProgressLayer.removeAllAnimations() + self.maskBlobView.stopAnimating() + return + } self.setupGradientAnimations() 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 bigBlob: BlobView @@ -678,9 +684,7 @@ private final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDec minSpeed: 0.85, maxSpeed: 7, minScale: mediumBlobRange.min, - maxScale: mediumBlobRange.max, - scaleSpeed: 0.2, - isCircle: false + maxScale: mediumBlobRange.max ) self.bigBlob = BlobView( pointsCount: 8, @@ -689,9 +693,7 @@ private final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDec minSpeed: 0.85, maxSpeed: 7, minScale: bigBlobRange.min, - maxScale: bigBlobRange.max, - scaleSpeed: 0.2, - isCircle: false + maxScale: bigBlobRange.max ) super.init(frame: frame) @@ -783,11 +785,6 @@ final class BlobView: UIView { let minScale: CGFloat let maxScale: CGFloat - let scaleSpeed: CGFloat - - var scaleLevelsToBalance = [CGFloat]() - - let isCircle: Bool var level: CGFloat = 0 { didSet { @@ -800,10 +797,7 @@ final class BlobView: UIView { } private var speedLevel: CGFloat = 0 - private var scaleLevel: CGFloat = 0 - private var lastSpeedLevel: CGFloat = 0 - private var lastScaleLevel: CGFloat = 0 private let shapeLayer: CAShapeLayer = { let layer = CAShapeLayer() @@ -841,9 +835,7 @@ final class BlobView: UIView { minSpeed: CGFloat, maxSpeed: CGFloat, minScale: CGFloat, - maxScale: CGFloat, - scaleSpeed: CGFloat, - isCircle: Bool + maxScale: CGFloat ) { self.pointsCount = pointsCount self.minRandomness = minRandomness @@ -852,8 +844,6 @@ final class BlobView: UIView { self.maxSpeed = maxSpeed self.minScale = minScale self.maxScale = maxScale - self.scaleSpeed = scaleSpeed - self.isCircle = isCircle let angle = (CGFloat.pi * 2) / CGFloat(pointsCount) self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2 @@ -876,7 +866,7 @@ final class BlobView: UIView { func updateSpeedLevel(to newSpeedLevel: CGFloat) { speedLevel = max(speedLevel, newSpeedLevel) - if abs(lastSpeedLevel - newSpeedLevel) > 0.5 { + if abs(lastSpeedLevel - newSpeedLevel) > 0.3 { animateToNewShape() } } @@ -892,8 +882,6 @@ final class BlobView: UIView { } private func animateToNewShape() { - guard !isCircle else { return } - if pop_animation(forKey: "blob") != nil { fromPoints = currentPoints toPoints = nil @@ -983,90 +971,6 @@ final class BlobView: UIView { CATransaction.begin() CATransaction.setDisableActions(true) 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() } } - - - - - - -//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 -// } -// } -// } -//} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index d835f759cc..4cd42d6b0f 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -144,23 +144,17 @@ public final class VoiceChatController: ViewController { } } - func updateAudioLevels(_ levels: [(PeerId, Float)], accountPeerId: PeerId, updateAll: Bool) { + func updateAudioLevels(_ levels: [(PeerId, Float)]) { var updated = Set() for (peerId, level) in levels { if let pipe = self.audioLevels[peerId] { - var level = level - if peerId != accountPeerId { - level = max(0.001, level) - } - pipe.putNext(level) + pipe.putNext(max(0.001, level)) updated.insert(peerId) } } - if updateAll { - for (peerId, pipe) in self.audioLevels { - if !updated.contains(peerId) && peerId != accountPeerId { - pipe.putNext(0.0) - } + for (peerId, pipe) in self.audioLevels { + if !updated.contains(peerId) { + pipe.putNext(0.0) } } } @@ -634,12 +628,12 @@ public final class VoiceChatController: ViewController { } }) - self.audioLevelsDisposable = (call.speakingAudioLevels + self.audioLevelsDisposable = (call.audioLevels |> deliverOnMainQueue).start(next: { [weak self] levels in guard let strongSelf = self else { return } - strongSelf.itemInteraction?.updateAudioLevels(levels, accountPeerId: strongSelf.context.account.peerId, updateAll: true) + strongSelf.itemInteraction?.updateAudioLevels(levels) }) self.myAudioLevelDisposable = (call.myAudioLevel @@ -651,7 +645,6 @@ public final class VoiceChatController: ViewController { if let state = strongSelf.callState, state.muteState == nil || strongSelf.pushingToTalk { effectiveLevel = level } - strongSelf.itemInteraction?.updateAudioLevels([(strongSelf.context.account.peerId, effectiveLevel)], accountPeerId: strongSelf.context.account.peerId, updateAll: false) strongSelf.actionButton.updateLevel(CGFloat(effectiveLevel)) }) @@ -789,6 +782,8 @@ public final class VoiceChatController: ViewController { override func didLoad() { super.didLoad() + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.actionButtonPressGesture(_:))) longTapRecognizer.minimumPressDuration = 0.001 longTapRecognizer.delegate = self @@ -815,7 +810,7 @@ public final class VoiceChatController: ViewController { } @objc private func leavePressed() { - self.hapticFeedback.impact(.veryLight) + self.hapticFeedback.impact(.light) self.leaveDisposable.set((self.call.leave(terminateIfPossible: false) |> 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 override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { @@ -844,7 +845,7 @@ public final class VoiceChatController: ViewController { } switch gestureRecognizer.state { case .began: - self.hapticFeedback.impact(.veryLight) + self.hapticFeedback.impact(.light) self.actionButtonPressGestureStartTime = CACurrentMediaTime() 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()) case .ended, .cancelled: - self.hapticFeedback.impact(.veryLight) + self.hapticFeedback.impact(.light) self.pushingToTalk = false self.actionButton.pressing = false @@ -887,7 +888,7 @@ public final class VoiceChatController: ViewController { } @objc private func audioOutputPressed() { - self.hapticFeedback.impact(.veryLight) + self.hapticFeedback.impact(.light) guard let (availableOutputs, currentOutput) = self.audioOutputState else { return @@ -1067,7 +1068,7 @@ public final class VoiceChatController: ViewController { soundImage = .speaker case .speaker: soundImage = .speaker -// soundAppearance = .blurred(isFilled: true) + soundAppearance = .blurred(isFilled: true) case .headphones: soundImage = .bluetooth case let .bluetooth(type): @@ -1228,7 +1229,7 @@ public final class VoiceChatController: ViewController { var memberMuteState: GroupCallParticipantsContext.Participant.MuteState? if member.peer.id == self.context.account.peerId { if muteState == nil { - memberState = .speaking + memberState = speakingPeers.contains(member.peer.id) ? .speaking : .listening } else { memberState = .listening memberMuteState = member.muteState