mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			199 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			199 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | |
| 
 | |
| import UIKit
 | |
| import WebRTC
 | |
| import SessionUIKit
 | |
| import SessionMessagingKit
 | |
| 
 | |
| 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 profilePictureView: ProfilePictureView = {
 | |
|         let result = ProfilePictureView()
 | |
|         let size = CGFloat(60)
 | |
|         result.size = size
 | |
|         result.set(.width, to: size)
 | |
|         result.set(.height, to: size)
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     private lazy var displayNameLabel: UILabel = {
 | |
|         let result = UILabel()
 | |
|         result.textColor = UIColor.white
 | |
|         result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
 | |
|         result.lineBreakMode = .byTruncatingTail
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     private lazy var answerButton: UIButton = {
 | |
|         let result = UIButton(type: .custom)
 | |
|         let image = UIImage(named: "AnswerCall")!.withTint(.white)?.resizedImage(to: CGSize(width: 24.8, height: 24.8))
 | |
|         result.setImage(image, for: UIControl.State.normal)
 | |
|         result.set(.width, to: 48)
 | |
|         result.set(.height, to: 48)
 | |
|         result.backgroundColor = Colors.accent
 | |
|         result.layer.cornerRadius = 24
 | |
|         result.addTarget(self, action: #selector(answerCall), for: UIControl.Event.touchUpInside)
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     private lazy var hangUpButton: UIButton = {
 | |
|         let result = UIButton(type: .custom)
 | |
|         let image = UIImage(named: "EndCall")!.withTint(.white)?.resizedImage(to: CGSize(width: 29.6, height: 11.2))
 | |
|         result.setImage(image, for: UIControl.State.normal)
 | |
|         result.set(.width, to: 48)
 | |
|         result.set(.height, to: 48)
 | |
|         result.backgroundColor = Colors.destructive
 | |
|         result.layer.cornerRadius = 24
 | |
|         result.addTarget(self, action: #selector(endCall), for: UIControl.Event.touchUpInside)
 | |
|         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.backgroundColor = UIColor(hex: 0x000000).withAlphaComponent(0.8)
 | |
|         self.layer.cornerRadius = Values.largeSpacing
 | |
|         self.layer.masksToBounds = true
 | |
|         self.set(.height, to: 100)
 | |
|         
 | |
|         profilePictureView.update(
 | |
|             publicKey: call.sessionId,
 | |
|             profile: Profile.fetchOrCreate(id: call.sessionId),
 | |
|             threadVariant: .contact
 | |
|         )
 | |
|         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
 | |
|         } else {
 | |
|             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) {
 | |
|             if answer { 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()
 | |
|         })
 | |
|     }
 | |
| 
 | |
| }
 |