|  |  |  | // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import UIKit | 
					
						
							|  |  |  | import SessionUIKit | 
					
						
							|  |  |  | import SessionMessagingKit | 
					
						
							|  |  |  | import SignalUtilitiesKit | 
					
						
							|  |  |  | import SessionUtilitiesKit | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { | 
					
						
							|  |  |  |     private static let swipeToOperateThreshold: CGFloat = 60 | 
					
						
							|  |  |  |     private var previousY: CGFloat = 0 | 
					
						
							|  |  |  |     let call: SessionCall | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - UI Components | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var backgroundView: UIView = { | 
					
						
							|  |  |  |         let result: UIView = UIView() | 
					
						
							|  |  |  |         result.themeBackgroundColor = .black | 
					
						
							|  |  |  |         result.alpha = 0.8 | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var profilePictureView: ProfilePictureView = ProfilePictureView(size: .list) | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var displayNameLabel: UILabel = { | 
					
						
							|  |  |  |         let result = UILabel() | 
					
						
							|  |  |  |         result.font = .boldSystemFont(ofSize: Values.mediumFontSize) | 
					
						
							|  |  |  |         result.themeTextColor = .white | 
					
						
							|  |  |  |         result.lineBreakMode = .byTruncatingTail | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var answerButton: UIButton = { | 
					
						
							|  |  |  |         let result = UIButton(type: .custom) | 
					
						
							|  |  |  |         result.setImage( | 
					
						
							|  |  |  |             UIImage(named: "AnswerCall")? | 
					
						
							|  |  |  |                 .resizedImage(to: CGSize(width: 24.8, height: 24.8))? | 
					
						
							|  |  |  |                 .withRenderingMode(.alwaysTemplate), | 
					
						
							|  |  |  |             for: .normal | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         result.themeTintColor = .white | 
					
						
							|  |  |  |         result.themeBackgroundColor = .callAccept_background | 
					
						
							|  |  |  |         result.layer.cornerRadius = 24 | 
					
						
							|  |  |  |         result.addTarget(self, action: #selector(answerCall), for: .touchUpInside) | 
					
						
							|  |  |  |         result.set(.width, to: 48) | 
					
						
							|  |  |  |         result.set(.height, to: 48) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var hangUpButton: UIButton = { | 
					
						
							|  |  |  |         let result = UIButton(type: .custom) | 
					
						
							|  |  |  |         result.setImage( | 
					
						
							|  |  |  |             UIImage(named: "EndCall")? | 
					
						
							|  |  |  |                 .resizedImage(to: CGSize(width: 29.6, height: 11.2))? | 
					
						
							|  |  |  |                 .withRenderingMode(.alwaysTemplate), | 
					
						
							|  |  |  |             for: .normal | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         result.themeTintColor = .white | 
					
						
							|  |  |  |         result.themeBackgroundColor = .callDecline_background | 
					
						
							|  |  |  |         result.layer.cornerRadius = 24 | 
					
						
							|  |  |  |         result.addTarget(self, action: #selector(endCall), for: .touchUpInside) | 
					
						
							|  |  |  |         result.set(.width, to: 48) | 
					
						
							|  |  |  |         result.set(.height, to: 48) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var panGestureRecognizer: UIPanGestureRecognizer = { | 
					
						
							|  |  |  |         let result = UIPanGestureRecognizer(target: self, action: #selector(handlePan)) | 
					
						
							|  |  |  |         result.delegate = self | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Initialization | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     public static var current: IncomingCallBanner? | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     init(for call: SessionCall) { | 
					
						
							|  |  |  |         self.call = call | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         super.init(frame: CGRect.zero) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         setUpViewHierarchy() | 
					
						
							|  |  |  |         setUpGestureRecognizers() | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         if let incomingCallBanner = IncomingCallBanner.current { | 
					
						
							|  |  |  |             incomingCallBanner.dismiss() | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         IncomingCallBanner.current = self | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     override init(frame: CGRect) { | 
					
						
							|  |  |  |         preconditionFailure("Use init(message:) instead.") | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     required init?(coder: NSCoder) { | 
					
						
							|  |  |  |         preconditionFailure("Use init(coder:) instead.") | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private func setUpViewHierarchy() { | 
					
						
							|  |  |  |         self.clipsToBounds = true | 
					
						
							|  |  |  |         self.layer.cornerRadius = Values.largeSpacing | 
					
						
							|  |  |  |         self.set(.height, to: 100) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         addSubview(backgroundView) | 
					
						
							|  |  |  |         backgroundView.pin(to: self) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         profilePictureView.update( | 
					
						
							|  |  |  |             publicKey: call.sessionId, | 
					
						
							|  |  |  |             threadVariant: .contact, | 
					
						
							|  |  |  |             customImageData: nil, | 
					
						
							|  |  |  |             profile: Storage.shared.read { db in Profile.fetchOrCreate(db, id: call.sessionId) }, | 
					
						
							|  |  |  |             additionalProfile: nil | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         displayNameLabel.text = call.contactName | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         let stackView = UIStackView(arrangedSubviews: [profilePictureView, displayNameLabel, hangUpButton, answerButton]) | 
					
						
							|  |  |  |         stackView.axis = .horizontal | 
					
						
							|  |  |  |         stackView.alignment = .center | 
					
						
							|  |  |  |         stackView.spacing = Values.largeSpacing | 
					
						
							|  |  |  |         self.addSubview(stackView) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         stackView.center(.vertical, in: self) | 
					
						
							|  |  |  |         stackView.autoPinWidthToSuperview(withMargin: Values.mediumSpacing) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private func setUpGestureRecognizers() { | 
					
						
							|  |  |  |         let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) | 
					
						
							|  |  |  |         tapGestureRecognizer.numberOfTapsRequired = 1 | 
					
						
							|  |  |  |         addGestureRecognizer(tapGestureRecognizer) | 
					
						
							|  |  |  |         addGestureRecognizer(panGestureRecognizer) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Interaction | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { | 
					
						
							|  |  |  |         if gestureRecognizer == panGestureRecognizer { | 
					
						
							|  |  |  |             let v = panGestureRecognizer.velocity(in: self) | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             return abs(v.y) > abs(v.x) // It has to be more vertical than horizontal | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return true | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { | 
					
						
							|  |  |  |         showCallVC(answer: false) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { | 
					
						
							|  |  |  |         let translationY = gestureRecognizer.translation(in: self).y | 
					
						
							|  |  |  |         switch gestureRecognizer.state { | 
					
						
							|  |  |  |             case .changed: | 
					
						
							|  |  |  |                 self.transform = CGAffineTransform(translationX: 0, y: min(translationY, IncomingCallBanner.swipeToOperateThreshold)) | 
					
						
							|  |  |  |                 if abs(translationY) > IncomingCallBanner.swipeToOperateThreshold && abs(previousY) < IncomingCallBanner.swipeToOperateThreshold { | 
					
						
							|  |  |  |                     UIImpactFeedbackGenerator(style: .heavy).impactOccurred() // Let the user know when they've hit the swipe to reply threshold | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 previousY = translationY | 
					
						
							|  |  |  |                  | 
					
						
							|  |  |  |             case .ended, .cancelled: | 
					
						
							|  |  |  |                 if abs(translationY) > IncomingCallBanner.swipeToOperateThreshold { | 
					
						
							|  |  |  |                     if translationY > 0 { | 
					
						
							|  |  |  |                         showCallVC(answer: false) | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                     else { | 
					
						
							|  |  |  |                         endCall()   // TODO: Or just put the call on hold? | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 else { | 
					
						
							|  |  |  |                     self.transform = .identity | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                  | 
					
						
							|  |  |  |             default: break | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     @objc private func answerCall() { | 
					
						
							|  |  |  |         showCallVC(answer: true) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     @objc private func endCall() { | 
					
						
							|  |  |  |         AppEnvironment.shared.callManager.endCall(call) { error in | 
					
						
							|  |  |  |             if let _ = error { | 
					
						
							|  |  |  |                 self.call.endSessionCall() | 
					
						
							|  |  |  |                 AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: nil) | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             self.dismiss() | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     public func showCallVC(answer: Bool) { | 
					
						
							|  |  |  |         dismiss() | 
					
						
							|  |  |  |         guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // FIXME: Handle more gracefully | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         let callVC = CallVC(for: self.call) | 
					
						
							|  |  |  |         if let conversationVC = presentingVC as? ConversationVC { | 
					
						
							|  |  |  |             callVC.conversationVC = conversationVC | 
					
						
							|  |  |  |             conversationVC.inputAccessoryView?.isHidden = true | 
					
						
							|  |  |  |             conversationVC.inputAccessoryView?.alpha = 0 | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         presentingVC.present(callVC, animated: true) { [weak self] in | 
					
						
							|  |  |  |             guard answer else { return } | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             self?.call.answerSessionCall() | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     public func show() { | 
					
						
							|  |  |  |         self.alpha = 0.0 | 
					
						
							|  |  |  |         let window = CurrentAppContext().mainWindow! | 
					
						
							|  |  |  |         window.addSubview(self) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         let topMargin = window.safeAreaInsets.top - Values.smallSpacing | 
					
						
							|  |  |  |         self.autoPinWidthToSuperview(withMargin: Values.smallSpacing) | 
					
						
							|  |  |  |         self.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: { | 
					
						
							|  |  |  |             self.alpha = 1.0 | 
					
						
							|  |  |  |         }, completion: nil) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         CallRingTonePlayer.shared.startVibration() | 
					
						
							|  |  |  |         CallRingTonePlayer.shared.startPlayingRingTone() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     public func dismiss() { | 
					
						
							|  |  |  |         CallRingTonePlayer.shared.stopVibrationIfPossible() | 
					
						
							|  |  |  |         CallRingTonePlayer.shared.stopPlayingRingTone() | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: { | 
					
						
							|  |  |  |             self.alpha = 0.0 | 
					
						
							|  |  |  |         }, completion: { _ in | 
					
						
							|  |  |  |             IncomingCallBanner.current = nil | 
					
						
							|  |  |  |             self.removeFromSuperview() | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | } |