TM-SGNL-iOS/Signal/Calls/UserInterface/CallControlsConfirmationToast.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

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