Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/Calls/UserInterface/CallControlsConfirmationToast.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import UIKit

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 = .dynamicTypeSubheadline
        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
    }

    func forceDismissToast() {
        if let toast {
            self.disappearAnimator(toast: toast).startAnimation()
        }
    }
}