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.
269 lines
9.6 KiB
Swift
269 lines
9.6 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
|
|
|
|
private let dependencies: Dependencies
|
|
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 = UIButton(primaryAction: UIAction { [weak self] _ in self?.answerCall() })
|
|
.withConfiguration(
|
|
UIButton.Configuration
|
|
.plain()
|
|
.withImage(UIImage(named: "AnswerCall")?.withRenderingMode(.alwaysTemplate))
|
|
.withContentInsets(NSDirectionalEdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6))
|
|
)
|
|
.withConfigurationUpdateHandler { button in
|
|
switch button.state {
|
|
case .highlighted: button.imageView?.tintAdjustmentMode = .dimmed
|
|
default: button.imageView?.tintAdjustmentMode = .normal
|
|
}
|
|
}
|
|
.withImageViewContentMode(.scaleAspectFit)
|
|
.withThemeTintColor(.white)
|
|
.withThemeBackgroundColor(.callAccept_background)
|
|
.withAccessibility(
|
|
identifier: "Close button",
|
|
label: "Close button"
|
|
)
|
|
.withCornerRadius(24)
|
|
.with(.width, of: 48)
|
|
.with(.height, of: 48)
|
|
|
|
private lazy var hangUpButton: UIButton = UIButton(primaryAction: UIAction { [weak self] _ in self?.endCall() })
|
|
.withConfiguration(
|
|
UIButton.Configuration
|
|
.plain()
|
|
.withImage(UIImage(named: "EndCall")?.withRenderingMode(.alwaysTemplate))
|
|
.withContentInsets(NSDirectionalEdgeInsets(top: 13, leading: 9, bottom: 13, trailing: 9))
|
|
)
|
|
.withConfigurationUpdateHandler { button in
|
|
switch button.state {
|
|
case .highlighted: button.imageView?.tintAdjustmentMode = .dimmed
|
|
default: button.imageView?.tintAdjustmentMode = .normal
|
|
}
|
|
}
|
|
.withImageViewContentMode(.scaleAspectFit)
|
|
.withThemeTintColor(.white)
|
|
.withThemeBackgroundColor(.callDecline_background)
|
|
.withAccessibility(
|
|
identifier: "Close button",
|
|
label: "Close button"
|
|
)
|
|
.withCornerRadius(24)
|
|
.with(.width, of: 48)
|
|
.with(.height, of: 48)
|
|
|
|
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, using dependencies: Dependencies) {
|
|
self.dependencies = dependencies
|
|
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 = 16
|
|
self.set(.height, to: 80)
|
|
|
|
addSubview(backgroundView)
|
|
backgroundView.pin(to: self)
|
|
|
|
profilePictureView.update(
|
|
publicKey: call.sessionId,
|
|
threadVariant: .contact,
|
|
displayPictureFilename: nil,
|
|
profile: dependencies[singleton: .storage].read { [sessionId = call.sessionId] db in
|
|
Profile.fetchOrCreate(db, id: sessionId)
|
|
},
|
|
additionalProfile: nil,
|
|
using: dependencies
|
|
)
|
|
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.pin(.leading, to: .leading, of: self, withInset: Values.mediumSpacing)
|
|
stackView.pin(.trailing, to: .trailing, of: self, withInset: -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: [CALLS] Or just put the call on hold?
|
|
}
|
|
}
|
|
else {
|
|
self.transform = .identity
|
|
}
|
|
|
|
default: break
|
|
}
|
|
}
|
|
|
|
private func answerCall() {
|
|
showCallVC(answer: true)
|
|
}
|
|
|
|
private func endCall() {
|
|
dependencies[singleton: .callManager].endCall(call) { [weak self, dependencies] error in
|
|
if let _ = error {
|
|
self?.call.endSessionCall()
|
|
dependencies[singleton: .callManager].reportCurrentCallEnded(reason: nil)
|
|
}
|
|
|
|
self?.dismiss()
|
|
}
|
|
}
|
|
|
|
public func showCallVC(answer: Bool) {
|
|
dismiss()
|
|
guard let presentingVC: UIViewController = dependencies[singleton: .appContext].frontMostViewController else {
|
|
Log.critical(.calls, "Failed to retrieve front view controller when showing the call UI")
|
|
return endCall()
|
|
}
|
|
|
|
let callVC = CallVC(for: self.call, using: dependencies)
|
|
if let conversationVC = (presentingVC as? TopBannerController)?.wrappedViewController() as? ConversationVC {
|
|
callVC.conversationVC = conversationVC
|
|
conversationVC.resignFirstResponder()
|
|
conversationVC.hideInputAccessoryView()
|
|
}
|
|
|
|
presentingVC.present(callVC, animated: true) { [weak self] in
|
|
guard answer else { return }
|
|
|
|
self?.call.answerSessionCall()
|
|
}
|
|
}
|
|
|
|
public func show() {
|
|
self.alpha = 0.0
|
|
|
|
guard let window: UIWindow = dependencies[singleton: .appContext].mainWindow else { return }
|
|
|
|
window.addSubview(self)
|
|
|
|
let topMargin = window.safeAreaInsets.top - Values.smallSpacing
|
|
self.set(.width, to: .width, of: window, withOffset: -Values.smallSpacing)
|
|
self.pin(.top, to: .top, of: window, withInset: topMargin)
|
|
self.center(.horizontal, in: window)
|
|
|
|
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()
|
|
})
|
|
}
|
|
}
|