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.
		
		
		
		
		
			
		
			
				
	
	
		
			249 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			249 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Swift
		
	
// 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 { [sessionId = call.sessionId] db in Profile.fetchOrCreate(db, id: 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
 | 
						|
            Singleton.hasAppContext,
 | 
						|
            let presentingVC = Singleton.appContext.frontmostViewController
 | 
						|
        else { preconditionFailure() } // FIXME: Handle more gracefully
 | 
						|
        
 | 
						|
        let callVC = CallVC(for: self.call)
 | 
						|
        if let conversationVC = (presentingVC as? TopBannerController)?.wrappedViewController() 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() {
 | 
						|
        guard Singleton.hasAppContext, let window: UIWindow = Singleton.appContext.mainWindow else { return }
 | 
						|
        
 | 
						|
        self.alpha = 0.0
 | 
						|
        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()
 | 
						|
        })
 | 
						|
    }
 | 
						|
 | 
						|
}
 |