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()
|
|
})
|
|
}
|
|
|
|
}
|