session-ios/Session/Calls/Views & Modals/IncomingCallBanner.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()
})
}
}