|  |  |  | // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import UIKit | 
					
						
							|  |  |  | import SessionUIKit | 
					
						
							|  |  |  | import SessionUtilitiesKit | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @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) { | 
					
						
							|  |  |  |         fatalError("init(coder:) has not been implemented") | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @available(*, unavailable, message:"use other constructor instead.") | 
					
						
							|  |  |  |     override init(frame: CGRect) { | 
					
						
							|  |  |  |         fatalError("init(frame:) has not been implemented") | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @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: .sessionDidBecomeActive, | 
					
						
							|  |  |  |                                                object: nil) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     deinit { | 
					
						
							|  |  |  |         NotificationCenter.default.removeObserver(self) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // MARK: - Notifications | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @objc func didBecomeActive() { | 
					
						
							|  |  |  |         Log.assertOnMainThread() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // 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) { | 
					
						
							|  |  |  |             fatalError("init(coder:) has not been implemented") | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         @available(*, unavailable, message:"use other constructor instead.") | 
					
						
							|  |  |  |         override init(frame: CGRect) { | 
					
						
							|  |  |  |             fatalError("init(frame:) has not been implemented") | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         init(dotType: DotType) { | 
					
						
							|  |  |  |             self.dotType = dotType | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             super.init(frame: .zero) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             set(.width, to: kMaxRadiusPt) | 
					
						
							|  |  |  |             set(.height, to: 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.color(for: .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(progress.clamp01().lerp(0.4, 1.0)) | 
					
						
							|  |  |  |                 colorValues.append(dotColor.cgColor) | 
					
						
							|  |  |  |                 let radius = progress.clamp01().lerp(TypingIndicatorView.kMinRadiusPt, TypingIndicatorView.kMaxRadiusPt) | 
					
						
							|  |  |  |                 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), // stringlint:disable | 
					
						
							|  |  |  |                 makeAnimation("path", pathValues) // stringlint:disable | 
					
						
							|  |  |  |             ] | 
					
						
							|  |  |  |             groupAnimation.duration = animationDuration | 
					
						
							|  |  |  |             groupAnimation.repeatCount = MAXFLOAT | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             shapeLayer.add(groupAnimation, forKey: UUID().uuidString) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         fileprivate func stopAnimation() { | 
					
						
							|  |  |  |             shapeLayer.removeAllAnimations() | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |