mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
c82ee0c44b
Added the Blocked contacts screen Added a setting to control whether concurrent audio messages should auto-play Finished updating the settings screens Fixed an issue where items that should be removed from the PagedDatabaseObserver due to filter logic weren't getting removed in some cases
215 lines
7.1 KiB
Swift
215 lines
7.1 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import SessionUIKit
|
|
|
|
@objc class TypingIndicatorView: UIStackView {
|
|
// This represents the spacing between the dots
|
|
// _at their max size_.
|
|
private let kDotMaxHSpacing: CGFloat = 3
|
|
|
|
@objc
|
|
public static let kMinRadiusPt: CGFloat = 6
|
|
@objc
|
|
public static let kMaxRadiusPt: CGFloat = 8
|
|
|
|
private let dot1 = DotView(dotType: .dotType1)
|
|
private let dot2 = DotView(dotType: .dotType2)
|
|
private let dot3 = DotView(dotType: .dotType3)
|
|
|
|
@available(*, unavailable, message:"use other constructor instead.")
|
|
required init(coder aDecoder: NSCoder) {
|
|
notImplemented()
|
|
}
|
|
|
|
@available(*, unavailable, message:"use other constructor instead.")
|
|
override init(frame: CGRect) {
|
|
notImplemented()
|
|
}
|
|
|
|
@objc
|
|
public init() {
|
|
super.init(frame: .zero)
|
|
|
|
// init(arrangedSubviews:...) is not a designated initializer.
|
|
for dot in dots() {
|
|
addArrangedSubview(dot)
|
|
}
|
|
|
|
self.axis = .horizontal
|
|
self.spacing = kDotMaxHSpacing
|
|
self.alignment = .center
|
|
|
|
NotificationCenter.default.addObserver(self,
|
|
selector: #selector(didBecomeActive),
|
|
name: NSNotification.Name.OWSApplicationDidBecomeActive,
|
|
object: nil)
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
// MARK: - Notifications
|
|
|
|
@objc func didBecomeActive() {
|
|
AssertIsOnMainThread()
|
|
|
|
// CoreAnimation animations are stopped in the background, so ensure
|
|
// animations are restored if necessary.
|
|
if isAnimating {
|
|
startAnimation()
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public override func sizeThatFits(_ size: CGSize) -> CGSize {
|
|
return CGSize(width: TypingIndicatorView.kMaxRadiusPt * 3 + kDotMaxHSpacing * 2, height: TypingIndicatorView.kMaxRadiusPt)
|
|
}
|
|
|
|
private func dots() -> [DotView] {
|
|
return [dot1, dot2, dot3]
|
|
}
|
|
|
|
private var isAnimating = false
|
|
|
|
@objc
|
|
public func startAnimation() {
|
|
isAnimating = true
|
|
|
|
for dot in dots() {
|
|
dot.startAnimation()
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func stopAnimation() {
|
|
isAnimating = false
|
|
|
|
for dot in dots() {
|
|
dot.stopAnimation()
|
|
}
|
|
}
|
|
|
|
private enum DotType {
|
|
case dotType1
|
|
case dotType2
|
|
case dotType3
|
|
}
|
|
|
|
private class DotView: UIView {
|
|
private let dotType: DotType
|
|
|
|
private let shapeLayer = CAShapeLayer()
|
|
|
|
@available(*, unavailable, message:"use other constructor instead.")
|
|
required init?(coder aDecoder: NSCoder) {
|
|
notImplemented()
|
|
}
|
|
|
|
@available(*, unavailable, message:"use other constructor instead.")
|
|
override init(frame: CGRect) {
|
|
notImplemented()
|
|
}
|
|
|
|
init(dotType: DotType) {
|
|
self.dotType = dotType
|
|
|
|
super.init(frame: .zero)
|
|
|
|
autoSetDimension(.width, toSize: kMaxRadiusPt)
|
|
autoSetDimension(.height, toSize: kMaxRadiusPt)
|
|
|
|
layer.addSublayer(shapeLayer)
|
|
|
|
ThemeManager.onThemeChange(observer: self) { [weak self] _, _ in
|
|
guard self?.shapeLayer.animationKeys()?.isEmpty == false else { return }
|
|
|
|
self?.startAnimation()
|
|
}
|
|
}
|
|
|
|
fileprivate func startAnimation() {
|
|
stopAnimation()
|
|
|
|
let baseColor: UIColor = (ThemeManager.currentTheme.colors[.messageBubble_incomingText] ?? .white)
|
|
let timeIncrement: CFTimeInterval = 0.15
|
|
var colorValues = [CGColor]()
|
|
var pathValues = [CGPath]()
|
|
var keyTimes = [CFTimeInterval]()
|
|
var animationDuration: CFTimeInterval = 0
|
|
|
|
let addDotKeyFrame = { (keyFrameTime: CFTimeInterval, progress: CGFloat) in
|
|
let dotColor = baseColor.withAlphaComponent(CGFloatLerp(0.4, 1.0, CGFloatClamp01(progress)))
|
|
colorValues.append(dotColor.cgColor)
|
|
let radius = CGFloatLerp(TypingIndicatorView.kMinRadiusPt, TypingIndicatorView.kMaxRadiusPt, CGFloatClamp01(progress))
|
|
let margin = (TypingIndicatorView.kMaxRadiusPt - radius) * 0.5
|
|
let bezierPath = UIBezierPath(ovalIn: CGRect(x: margin, y: margin, width: radius, height: radius))
|
|
pathValues.append(bezierPath.cgPath)
|
|
|
|
keyTimes.append(keyFrameTime)
|
|
animationDuration = max(animationDuration, keyFrameTime)
|
|
}
|
|
|
|
// All animations in the group apparently need to have the same number
|
|
// of keyframes, and use the same timing.
|
|
switch dotType {
|
|
case .dotType1:
|
|
addDotKeyFrame(0 * timeIncrement, 0.0)
|
|
addDotKeyFrame(1 * timeIncrement, 0.5)
|
|
addDotKeyFrame(2 * timeIncrement, 1.0)
|
|
addDotKeyFrame(3 * timeIncrement, 0.5)
|
|
addDotKeyFrame(4 * timeIncrement, 0.0)
|
|
addDotKeyFrame(5 * timeIncrement, 0.0)
|
|
addDotKeyFrame(6 * timeIncrement, 0.0)
|
|
addDotKeyFrame(10 * timeIncrement, 0.0)
|
|
break
|
|
case .dotType2:
|
|
addDotKeyFrame(0 * timeIncrement, 0.0)
|
|
addDotKeyFrame(1 * timeIncrement, 0.0)
|
|
addDotKeyFrame(2 * timeIncrement, 0.5)
|
|
addDotKeyFrame(3 * timeIncrement, 1.0)
|
|
addDotKeyFrame(4 * timeIncrement, 0.5)
|
|
addDotKeyFrame(5 * timeIncrement, 0.0)
|
|
addDotKeyFrame(6 * timeIncrement, 0.0)
|
|
addDotKeyFrame(10 * timeIncrement, 0.0)
|
|
break
|
|
case .dotType3:
|
|
addDotKeyFrame(0 * timeIncrement, 0.0)
|
|
addDotKeyFrame(1 * timeIncrement, 0.0)
|
|
addDotKeyFrame(2 * timeIncrement, 0.0)
|
|
addDotKeyFrame(3 * timeIncrement, 0.5)
|
|
addDotKeyFrame(4 * timeIncrement, 1.0)
|
|
addDotKeyFrame(5 * timeIncrement, 0.5)
|
|
addDotKeyFrame(6 * timeIncrement, 0.0)
|
|
addDotKeyFrame(10 * timeIncrement, 0.0)
|
|
break
|
|
}
|
|
|
|
let makeAnimation: (String, [Any]) -> CAKeyframeAnimation = { (keyPath, values) in
|
|
let animation = CAKeyframeAnimation()
|
|
animation.keyPath = keyPath
|
|
animation.values = values
|
|
animation.duration = animationDuration
|
|
return animation
|
|
}
|
|
|
|
let groupAnimation = CAAnimationGroup()
|
|
groupAnimation.animations = [
|
|
makeAnimation("fillColor", colorValues),
|
|
makeAnimation("path", pathValues)
|
|
]
|
|
groupAnimation.duration = animationDuration
|
|
groupAnimation.repeatCount = MAXFLOAT
|
|
|
|
shapeLayer.add(groupAnimation, forKey: UUID().uuidString)
|
|
}
|
|
|
|
fileprivate func stopAnimation() {
|
|
shapeLayer.removeAllAnimations()
|
|
}
|
|
}
|
|
}
|