209 lines
7.7 KiB
Swift
209 lines
7.7 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalServiceKit
|
|
|
|
class CallControlsConfirmationToastView: UIView {
|
|
enum ControlType {
|
|
case mute(isOn: Bool)
|
|
case speakerphone(isOn: Bool)
|
|
case ring(isOn: Bool)
|
|
|
|
var imageName: String {
|
|
switch self {
|
|
case .mute(let isOn):
|
|
if isOn {
|
|
return "mic-slash"
|
|
} else {
|
|
return "mic"
|
|
}
|
|
case .speakerphone(let isOn):
|
|
if isOn {
|
|
return "speaker"
|
|
} else {
|
|
return "speaker-slash"
|
|
}
|
|
case .ring(let isOn):
|
|
if isOn {
|
|
return "bell"
|
|
} else {
|
|
return "bell-slash"
|
|
}
|
|
}
|
|
}
|
|
|
|
var text: String {
|
|
switch self {
|
|
case .mute(let isOn):
|
|
if isOn {
|
|
return OWSLocalizedString(
|
|
"MUTE_CONFIRMATION_TOAST_LABEL",
|
|
comment: "Text for a toast confirming that the mic has been muted for a call."
|
|
)
|
|
} else {
|
|
return OWSLocalizedString(
|
|
"UNMUTE_CONFIRMATION_TOAST_LABEL",
|
|
comment: "Text for a toast confirming that the mic has been unmuted for a call."
|
|
)
|
|
}
|
|
case .speakerphone(let isOn):
|
|
if isOn {
|
|
return OWSLocalizedString(
|
|
"SPEAKERPHONE_ON_CONFIRMATION_TOAST_LABEL",
|
|
comment: "Text for a toast confirming that the speakerphone has been turned on for a call."
|
|
)
|
|
} else {
|
|
return OWSLocalizedString(
|
|
"SPEAKERPHONE_OFF_CONFIRMATION_TOAST_LABEL",
|
|
comment: "Text for a toast confirming that the speakerphone has been turned off for a call."
|
|
)
|
|
}
|
|
case .ring(let isOn):
|
|
if isOn {
|
|
return OWSLocalizedString(
|
|
"RING_ON_CONFIRMATION_TOAST_LABEL",
|
|
comment: "Text for a toast confirming that ringing has been turned on for a call."
|
|
)
|
|
} else {
|
|
return OWSLocalizedString(
|
|
"RING_OFF_CONFIRMATION_TOAST_LABEL",
|
|
comment: "Text for a toast confirming that ringing has been turned off for a call."
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum Style {
|
|
private static let opacity = UIAccessibility.isReduceTransparencyEnabled ? 0.8 : 0.6
|
|
static let toastBackgroundColor = UIColor(red: 0.29, green: 0.29, blue: 0.29, alpha: opacity)
|
|
static let textAndImageColor = UIColor(red: 0.91, green: 0.91, blue: 0.91, alpha: 1)
|
|
static let cornerRadius: CGFloat = 20
|
|
static let spacing: CGFloat = 8
|
|
static let horizontalMargin: CGFloat = 12
|
|
static let verticalMargin: CGFloat = 10
|
|
static let font: UIFont = .dynamicTypeBody2
|
|
static let imageDimension: CGFloat = 16
|
|
}
|
|
|
|
init(state: ControlType) {
|
|
super.init(frame: .zero)
|
|
|
|
// Image view
|
|
let imageView = UIImageView(image: UIImage(named: state.imageName))
|
|
imageView.tintColor = Style.textAndImageColor
|
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
imageView.widthAnchor.constraint(equalToConstant: Style.imageDimension),
|
|
imageView.heightAnchor.constraint(equalToConstant: Style.imageDimension)
|
|
])
|
|
// Label
|
|
let label = UILabel()
|
|
label.textColor = Style.textAndImageColor
|
|
label.text = state.text
|
|
label.font = Style.font
|
|
// Stack
|
|
let stackView = UIStackView()
|
|
stackView.axis = .horizontal
|
|
stackView.spacing = Style.spacing
|
|
stackView.alignment = .center
|
|
stackView.addArrangedSubview(imageView)
|
|
stackView.addArrangedSubview(label)
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
// Blur view
|
|
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterialDark))
|
|
blurView.translatesAutoresizingMaskIntoConstraints = false
|
|
// Container view
|
|
let containerView = UIView()
|
|
containerView.backgroundColor = Style.toastBackgroundColor
|
|
containerView.translatesAutoresizingMaskIntoConstraints = false
|
|
containerView.addSubview(stackView)
|
|
NSLayoutConstraint.activate([
|
|
containerView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: -Style.horizontalMargin),
|
|
containerView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: Style.horizontalMargin),
|
|
containerView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: Style.verticalMargin),
|
|
containerView.topAnchor.constraint(equalTo: stackView.topAnchor, constant: -Style.verticalMargin),
|
|
])
|
|
// Self
|
|
self.addSubview(blurView)
|
|
self.addSubview(containerView)
|
|
self.layer.cornerRadius = Style.cornerRadius
|
|
self.clipsToBounds = true
|
|
blurView.autoPinEdgesToSuperviewEdges()
|
|
containerView.autoPinEdgesToSuperviewEdges()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
class CallControlsConfirmationToastManager {
|
|
typealias ControlType = CallControlsConfirmationToastView.ControlType
|
|
|
|
private let presentingContainerView: UIView
|
|
|
|
init(presentingContainerView: UIView) {
|
|
self.presentingContainerView = presentingContainerView
|
|
}
|
|
|
|
func toastInducingCallControlChangeDidOccur(state: ControlType) {
|
|
self.presentToast(from: self.presentingContainerView, state: state)
|
|
}
|
|
|
|
private var toast: UIView?
|
|
|
|
private func presentToast(from view: UIView, state: ControlType) {
|
|
if let oldToast = self.toast {
|
|
// Handle case where new toast is triggered before old
|
|
// toast's disappearance animation completes.
|
|
oldToast.layer.removeAllAnimations()
|
|
oldToast.removeFromSuperview()
|
|
}
|
|
|
|
let toast = CallControlsConfirmationToastView(state: state)
|
|
self.toast = toast
|
|
toast.alpha = 0
|
|
view.addSubview(toast)
|
|
toast.transform = .scale(0.8)
|
|
toast.autoPinEdgesToSuperviewEdges()
|
|
|
|
let appearAnimator = UIViewPropertyAnimator(
|
|
duration: 0.2,
|
|
springDamping: 0.8,
|
|
springResponse: 0.2
|
|
)
|
|
appearAnimator.addAnimations {
|
|
toast.alpha = 1
|
|
toast.transform = .identity
|
|
}
|
|
appearAnimator.addCompletion { _ in
|
|
let disappearAnimator = self.disappearAnimator(toast: toast)
|
|
disappearAnimator.startAnimation(afterDelay: 2)
|
|
}
|
|
appearAnimator.startAnimation()
|
|
}
|
|
|
|
private func disappearAnimator(toast: UIView) -> UIViewPropertyAnimator {
|
|
let disappearAnimator = UIViewPropertyAnimator(
|
|
duration: 0.2,
|
|
springDamping: 0.8,
|
|
springResponse: 0.2
|
|
)
|
|
disappearAnimator.addAnimations {
|
|
toast.alpha = 0
|
|
}
|
|
disappearAnimator.addCompletion { _ in
|
|
toast.removeFromSuperview()
|
|
}
|
|
return disappearAnimator
|
|
}
|
|
|
|
public func forceDismissToast() {
|
|
if let toast {
|
|
self.disappearAnimator(toast: toast).startAnimation()
|
|
}
|
|
}
|
|
}
|