[WIP] Private Call UI

This commit is contained in:
Ali
2023-11-13 21:51:22 +04:00
parent 04705d4d09
commit e26df40077
23 changed files with 1382 additions and 385 deletions

View File

@@ -1,6 +1,36 @@
import Foundation
import UIKit
import Display
import ComponentFlow
private func addRoundedRectPath(context: CGContext, rect: CGRect, radius: CGFloat) {
context.saveGState()
context.translateBy(x: rect.minX, y: rect.minY)
context.scaleBy(x: radius, y: radius)
let fw = rect.width / radius
let fh = rect.height / radius
context.move(to: CGPoint(x: fw, y: fh / 2.0))
context.addArc(tangent1End: CGPoint(x: fw, y: fh), tangent2End: CGPoint(x: fw/2, y: fh), radius: 1.0)
context.addArc(tangent1End: CGPoint(x: 0, y: fh), tangent2End: CGPoint(x: 0, y: fh/2), radius: 1)
context.addArc(tangent1End: CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: fw/2, y: 0), radius: 1)
context.addArc(tangent1End: CGPoint(x: fw, y: 0), tangent2End: CGPoint(x: fw, y: fh/2), radius: 1)
context.closePath()
context.restoreGState()
}
private func stringForDuration(_ duration: Int) -> String {
let hours = duration / 3600
let minutes = duration / 60 % 60
let seconds = duration % 60
let durationString: String
if hours > 0 {
durationString = String(format: "%d:%02d:%02d", hours, minutes, seconds)
} else {
durationString = String(format: "%d:%02d", minutes, seconds)
}
return durationString
}
private final class AnimatedDotsLayer: SimpleLayer {
private let dotLayers: [SimpleLayer]
@@ -71,28 +101,116 @@ private final class AnimatedDotsLayer: SimpleLayer {
}
}
private final class SignalStrengthView: UIView {
let barViews: [UIImageView]
let size: CGSize
override init(frame: CGRect) {
self.barViews = (0 ..< 4).map { _ in
return UIImageView()
}
let itemWidth: CGFloat = 3.0
let itemHeight: CGFloat = 12.0
let itemSpacing: CGFloat = 2.0
self.size = CGSize(width: CGFloat(self.barViews.count) * itemWidth + CGFloat(self.barViews.count - 1) * itemSpacing, height: itemHeight)
super.init(frame: frame)
let itemImage = UIGraphicsImageRenderer(size: CGSize(width: itemWidth, height: itemWidth)).image(actions: { context in
context.cgContext.setFillColor(UIColor.white.cgColor)
addRoundedRectPath(context: context.cgContext, rect: CGRect(origin: CGPoint(), size: CGSize(width: itemWidth, height: itemWidth)), radius: 1.0)
context.cgContext.fillPath()
}).stretchableImage(withLeftCapWidth: Int(itemWidth * 0.5), topCapHeight: Int(itemWidth * 0.5))
var nextX: CGFloat = 0.0
for i in 0 ..< self.barViews.count {
let barView = self.barViews[i]
barView.image = itemImage
let barHeight = floor(CGFloat(i + 1) * itemHeight / CGFloat(self.barViews.count))
barView.frame = CGRect(origin: CGPoint(x: nextX, y: itemHeight - barHeight), size: CGSize(width: itemWidth, height: barHeight))
nextX += itemSpacing + itemWidth
self.addSubview(barView)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(value: Double) {
for i in 0 ..< self.barViews.count {
if value >= Double(i + 1) / Double(self.barViews.count) {
self.barViews[i].alpha = 1.0
} else {
self.barViews[i].alpha = 0.5
}
}
}
}
final class StatusView: UIView {
private struct LayoutState: Equatable {
var state: State
var size: CGSize
init(state: State, size: CGSize) {
self.state = state
self.size = size
}
}
enum WaitingState {
case requesting
case ringing
case generatingKeys
}
struct ActiveState {
struct ActiveState: Equatable {
var startTimestamp: Double
var signalStrength: Double
init(signalStrength: Double) {
init(startTimestamp: Double, signalStrength: Double) {
self.startTimestamp = startTimestamp
self.signalStrength = signalStrength
}
}
enum State {
enum State: Equatable {
enum Key: Equatable {
case waiting(WaitingState)
case active
}
case waiting(WaitingState)
case active(ActiveState)
var key: Key {
switch self {
case let .waiting(waitingState):
return .waiting(waitingState)
case .active:
return .active
}
}
}
private var textView: TextView
private let textView: TextView
private var dotsLayer: AnimatedDotsLayer?
private var signalStrengthView: SignalStrengthView?
private var activeDurationTimer: Foundation.Timer?
private var layoutState: LayoutState?
var state: State? {
return self.layoutState?.state
}
var requestLayout: (() -> Void)?
override init(frame: CGRect) {
self.textView = TextView()
@@ -106,9 +224,60 @@ final class StatusView: UIView {
fatalError("init(coder:) has not been implemented")
}
func update(state: State) -> CGSize {
deinit {
self.activeDurationTimer?.invalidate()
}
func update(state: State, transition: Transition) -> CGSize {
if let layoutState = self.layoutState, layoutState.state == state {
return layoutState.size
}
let size = self.updateInternal(state: state, transition: transition)
self.layoutState = LayoutState(state: state, size: size)
self.updateActiveDurationTimer()
return size
}
private func updateActiveDurationTimer() {
if let layoutState = self.layoutState, case let .active(activeState) = layoutState.state {
if self.activeDurationTimer == nil {
let timestamp = Date().timeIntervalSince1970
let duration = timestamp - activeState.startTimestamp
let nextTickDelay = ceil(duration) - duration + 0.05
self.activeDurationTimer = Foundation.Timer.scheduledTimer(withTimeInterval: nextTickDelay, repeats: false, block: { [weak self] _ in
guard let self else {
return
}
self.activeDurationTimer?.invalidate()
self.activeDurationTimer = nil
if let layoutState = self.layoutState {
let size = self.updateInternal(state: layoutState.state, transition: .immediate)
if layoutState.size != size {
self.layoutState = nil
self.requestLayout?()
}
}
self.updateActiveDurationTimer()
})
}
} else {
if let activeDurationTimer = self.activeDurationTimer {
self.activeDurationTimer = nil
activeDurationTimer.invalidate()
}
}
}
private func updateInternal(state: State, transition: Transition) -> CGSize {
let textString: String
var needsDots = false
var monospacedDigits = false
var signalStrength: Double?
switch state {
case let .waiting(waitingState):
needsDots = true
@@ -122,15 +291,49 @@ final class StatusView: UIView {
textString = "Exchanging encryption keys"
}
case let .active(activeState):
textString = "0:00"
let _ = activeState
monospacedDigits = true
let timestamp = Date().timeIntervalSince1970
let duration = timestamp - activeState.startTimestamp
textString = stringForDuration(Int(duration))
signalStrength = activeState.signalStrength
}
let textSize = self.textView.update(string: textString, fontSize: 16.0, fontWeight: 0.0, constrainedWidth: 200.0)
self.textView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: textSize)
var contentSize = textSize
var contentSize = CGSize()
let dotsSpacing: CGFloat = 6.0
if let signalStrength {
let signalStrengthView: SignalStrengthView
if let current = self.signalStrengthView {
signalStrengthView = current
} else {
signalStrengthView = SignalStrengthView(frame: CGRect())
self.signalStrengthView = signalStrengthView
self.addSubview(signalStrengthView)
}
signalStrengthView.update(value: signalStrength)
contentSize.width += signalStrengthView.size.width + 7.0
} else {
if let signalStrengthView = self.signalStrengthView {
self.signalStrengthView = nil
signalStrengthView.removeFromSuperview()
}
}
let textSize = self.textView.update(string: textString, fontSize: 16.0, fontWeight: 0.0, monospacedDigits: monospacedDigits, color: .white, constrainedWidth: 250.0, transition: .immediate)
let textFrame = CGRect(origin: CGPoint(x: contentSize.width, y: 0.0), size: textSize)
if self.textView.bounds.isEmpty {
self.textView.frame = textFrame
} else {
transition.setPosition(view: self.textView, position: textFrame.center)
transition.setBounds(view: self.textView, bounds: CGRect(origin: CGPoint(), size: textFrame.size))
}
contentSize.width += textSize.width
contentSize.height = textSize.height
if let signalStrengthView = self.signalStrengthView {
transition.setFrame(view: signalStrengthView, frame: CGRect(origin: CGPoint(x: 0.0, y: floor((textSize.height - signalStrengthView.size.height) * 0.5)), size: signalStrengthView.size))
}
if needsDots {
let dotsLayer: AnimatedDotsLayer
@@ -140,13 +343,19 @@ final class StatusView: UIView {
dotsLayer = AnimatedDotsLayer()
self.dotsLayer = dotsLayer
self.layer.addSublayer(dotsLayer)
transition.animateAlpha(layer: dotsLayer, from: 0.0, to: 1.0)
}
dotsLayer.frame = CGRect(origin: CGPoint(x: textSize.width + dotsSpacing, y: 1.0 + floor((textSize.height - dotsLayer.size.height) * 0.5)), size: dotsLayer.size)
contentSize.width += dotsSpacing + dotsLayer.size.width
let dotsSpacing: CGFloat = 6.0
let dotsFrame = CGRect(origin: CGPoint(x: textSize.width + dotsSpacing, y: 1.0 + floor((textSize.height - dotsLayer.size.height) * 0.5)), size: dotsLayer.size)
transition.setFrame(layer: dotsLayer, frame: dotsFrame)
contentSize.width += dotsSpacing + dotsFrame.width
} else if let dotsLayer = self.dotsLayer {
self.dotsLayer = nil
dotsLayer.removeFromSuperlayer()
transition.setAlpha(layer: dotsLayer, alpha: 0.0, completion: { [weak dotsLayer] _ in
dotsLayer?.removeFromSuperlayer()
})
}
return contentSize