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

834 lines
29 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalRingRTC
import SignalServiceKit
import SignalUI
@objc
protocol CallControlsDelegate: AnyObject {
func didPressRing()
func didPressJoin()
func didPressHangup()
func didPressMore()
}
class CallControls: UIView {
private lazy var topStackView = createTopStackView()
private lazy var hangUpButton: CallButton = {
let button = createButton(
iconName: "phone-down-fill-28",
accessibilityLabel: viewModel.hangUpButtonAccessibilityLabel) { [viewModel] _ in
viewModel.didPressHangup()
}
button.unselectedBackgroundColor = UIColor(rgbHex: 0xEB5545)
return button
}()
private(set) lazy var audioSourceButton = createButton(
iconName: "speaker-fill-28",
accessibilityLabel: viewModel.audioSourceAccessibilityLabel) { [viewModel] _ in
viewModel.didPressAudioSource()
}
private lazy var muteButton = createButton(
iconName: "mic-fill",
selectedIconName: "mic-slash-fill-28",
accessibilityLabel: viewModel.muteButtonAccessibilityLabel) { [viewModel] _ in
viewModel.didPressMute()
}
private lazy var videoButton = createButton(
iconName: "video-fill-28",
selectedIconName: "video-slash-fill-28",
accessibilityLabel: viewModel.videoButtonAccessibilityLabel) { [viewModel] _ in
viewModel.didPressVideo()
}
private lazy var ringButton = createButton(
iconName: "bell-ring-fill-28",
selectedIconName: "bell-slash-fill",
accessibilityLabel: viewModel.ringButtonAccessibilityLabel) { [viewModel] _ in
viewModel.didPressRing()
}
private lazy var flipCameraButton: CallButton = {
let button = createButton(
iconName: "switch-camera-28",
accessibilityLabel: viewModel.flipCameraButtonAccessibilityLabel) { [viewModel] _ in
viewModel.didPressFlipCamera()
}
button.selectedIconColor = button.iconColor
button.selectedBackgroundColor = button.unselectedBackgroundColor
return button
}()
private lazy var moreButton = createButton(
iconName: "more",
accessibilityLabel: viewModel.moreButtonAccessibilityLabel) { [viewModel] _ in
viewModel.didPressMore()
}
private lazy var joinButtonActivityIndicator = UIActivityIndicatorView(style: .medium)
private lazy var joinButton: UIButton = {
let height: CGFloat = HeightConstants.joinButtonHeight
let button = OWSButton()
button.setTitleColor(.ows_white, for: .normal)
button.setBackgroundImage(UIImage.image(color: .ows_accentGreen), for: .normal)
button.titleLabel?.font = UIFont.dynamicTypeBodyClamped.semibold()
button.clipsToBounds = true
button.layer.cornerRadius = height / 2
button.block = { [weak self, unowned button] in
self?.viewModel.didPressJoin()
}
button.ows_contentEdgeInsets = UIEdgeInsets(top: 17, leading: 17, bottom: 17, trailing: 17)
button.addSubview(joinButtonActivityIndicator)
joinButtonActivityIndicator.autoCenterInSuperview()
// Expand the button to fit text if necessary.
button.autoSetDimension(.width, toSize: 168, relation: .greaterThanOrEqual)
button.autoSetDimension(.height, toSize: height)
return button
}()
static func joinButtonLabel(for call: SignalCall) -> String {
return CallControlsViewModel.joinButtonLabel(for: call)
}
private weak var delegate: CallControlsDelegate!
private let viewModel: CallControlsViewModel
init(
call: SignalCall,
callService: CallService,
confirmationToastManager: CallControlsConfirmationToastManager,
delegate: CallControlsDelegate
) {
let viewModel = CallControlsViewModel(
call: call,
callService: callService,
confirmationToastManager: confirmationToastManager,
delegate: delegate
)
self.viewModel = viewModel
self.delegate = delegate
super.init(frame: .zero)
viewModel.refreshView = { [weak self] in
self?.updateControls()
}
let joinButtonContainer = UIView()
joinButtonContainer.addSubview(joinButton)
joinButtonContainer.layoutMargins = UIEdgeInsets(hMargin: 16, vMargin: 0)
joinButton.autoPinWidthToSuperviewMargins(relation: .lessThanOrEqual)
joinButton.autoPinHeightToSuperview()
let controlsStack = UIStackView(arrangedSubviews: [
topStackView,
joinButtonContainer
])
controlsStack.axis = .vertical
controlsStack.spacing = HeightConstants.stackSpacing
controlsStack.alignment = .center
addSubview(controlsStack)
controlsStack.autoPinWidthToSuperview()
controlsStack.autoPinEdge(
toSuperviewSafeArea: .bottom,
withInset: HeightConstants.bottomPadding,
relation: .lessThanOrEqual
)
NSLayoutConstraint.autoSetPriority(.defaultHigh - 1) {
controlsStack.autoPinEdge(toSuperviewSafeArea: .bottom, withInset: 56)
}
controlsStack.autoPinEdge(toSuperviewEdge: .top)
updateControls()
}
func createTopStackView() -> UIStackView {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 16
stackView.addArrangedSubview(audioSourceButton)
stackView.addArrangedSubview(flipCameraButton)
stackView.addArrangedSubview(videoButton)
stackView.addArrangedSubview(muteButton)
stackView.addArrangedSubview(moreButton)
stackView.addArrangedSubview(ringButton)
stackView.addArrangedSubview(hangUpButton)
return stackView
}
private var heightAfterLastUpdate: CGFloat = 0
private var animator: UIViewPropertyAnimator?
private func updateControls() {
// Top row
hangUpButton.isHidden = viewModel.hangUpButtonIsHidden
muteButton.isHidden = viewModel.muteButtonIsHidden
moreButton.isHidden = viewModel.moreButtonIsHidden
videoButton.isHidden = viewModel.videoButtonIsHidden
flipCameraButton.isHidden = viewModel.flipCameraButtonIsHidden
ringButton.isHidden = viewModel.ringButtonIsHidden
// Bottom row
joinButton.superview?.isHidden = viewModel.joinButtonIsHidden
// Sizing and spacing
let controlCount = topStackView.arrangedSubviews.filter({!$0.isHidden}).count
topStackView.spacing = viewModel.controlSpacing(controlCount: controlCount)
let shouldControlButtonsBeSmall = viewModel.shouldControlButtonsBeSmall(controlCount: controlCount)
for view in topStackView.arrangedSubviews {
if let button = view as? CallButton {
button.isSmall = shouldControlButtonsBeSmall
}
}
videoButton.isSelected = viewModel.videoButtonIsSelected
muteButton.isSelected = viewModel.muteButtonIsSelected
audioSourceButton.isSelected = viewModel.audioSourceButtonIsSelected
ringButton.isSelected = viewModel.ringButtonIsSelected
flipCameraButton.isSelected = viewModel.flipCameraButtonIsSelected
moreButton.isSelected = viewModel.moreButtonIsSelected
if !viewModel.audioSourceButtonIsHidden {
let config = viewModel.audioSourceButtonConfiguration
audioSourceButton.showDropdownArrow = config.showDropdownArrow
audioSourceButton.iconName = config.iconName
}
if
!viewModel.ringButtonIsHidden,
let ringButtonConfig = viewModel.ringButtonConfiguration
{
ringButton.isUserInteractionEnabled = ringButtonConfig.isUserInteractionEnabled
ringButton.isSelected = ringButtonConfig.isSelected
ringButton.shouldDrawAsDisabled = ringButtonConfig.shouldDrawAsDisabled
}
if !viewModel.joinButtonIsHidden {
let joinButtonConfig = viewModel.joinButtonConfig
joinButton.setTitle(joinButtonConfig.label, for: .normal)
joinButton.setTitleColor(joinButtonConfig.color, for: .normal)
joinButton.ows_adjustsImageWhenHighlighted = joinButtonConfig.adjustsImageWhenHighlighted
joinButton.isUserInteractionEnabled = joinButtonConfig.isUserInteractionEnabled
if viewModel.shouldJoinButtonActivityIndicatorBeAnimating {
joinButtonActivityIndicator.startAnimating()
} else {
joinButtonActivityIndicator.stopAnimating()
}
}
hangUpButton.accessibilityLabel = viewModel.hangUpButtonAccessibilityLabel
audioSourceButton.accessibilityLabel = viewModel.audioSourceAccessibilityLabel
muteButton.accessibilityLabel = viewModel.muteButtonAccessibilityLabel
videoButton.accessibilityLabel = viewModel.videoButtonAccessibilityLabel
ringButton.accessibilityLabel = viewModel.ringButtonAccessibilityLabel
flipCameraButton.accessibilityLabel = viewModel.flipCameraButtonAccessibilityLabel
moreButton.accessibilityLabel = viewModel.moreButtonAccessibilityLabel
if self.heightAfterLastUpdate != self.currentHeight {
// callControlsHeightDidChange will animate changes
self.animator?.stopAnimation(true)
audioSourceButton.isHiddenInStackView = viewModel.audioSourceButtonIsHidden
callControlsHeightObservers.elements.forEach {
$0.callControlsHeightDidChange(newHeight: currentHeight)
}
} else if audioSourceButton.isHiddenInStackView != viewModel.audioSourceButtonIsHidden {
// Animate audioSourceButton ourselves
self.animator?.stopAnimation(true)
let animator = UIViewPropertyAnimator(
duration: 0.5,
controlPoint1: .init(x: 0.25, y: 1),
controlPoint2: .init(x: 0.25, y: 1)
)
animator.addAnimations { [unowned self] in
self.audioSourceButton.isHiddenInStackView = self.viewModel.audioSourceButtonIsHidden
}
animator.startAnimation()
self.animator = animator
}
self.heightAfterLastUpdate = self.currentHeight
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func createButton(
iconName: String,
selectedIconName: String? = nil,
accessibilityLabel: String? = nil,
action: @escaping UIActionHandler
) -> CallButton {
let button = CallButton(iconName: iconName)
button.selectedIconName = selectedIconName
button.accessibilityLabel = accessibilityLabel
button.addAction(UIAction(handler: action), for: .touchUpInside)
button.setContentHuggingHorizontalHigh()
button.setCompressionResistanceHorizontalLow()
return button
}
// MARK: Height Observing
private var callControlsHeightObservers: WeakArray<any CallControlsHeightObserver> = []
func addHeightObserver(_ observer: CallControlsHeightObserver) {
callControlsHeightObservers.append(observer)
}
var currentHeight: CGFloat {
var height = self.buttonRowHeight + HeightConstants.bottomPadding
if !viewModel.joinButtonIsHidden {
height += HeightConstants.joinButtonHeight
height += HeightConstants.stackSpacing
}
return height
}
private var buttonRowHeight: CGFloat {
self.muteButton.currentIconSize
}
private enum HeightConstants {
static let joinButtonHeight: CGFloat = 56
static let stackSpacing: CGFloat = 30
static let bottomPadding: CGFloat = 40
}
}
protocol CallControlsHeightObserver {
func callControlsHeightDidChange(newHeight: CGFloat)
}
// MARK: - View Model
@MainActor
private class CallControlsViewModel {
private let call: SignalCall
private let callService: CallService
private weak var delegate: CallControlsDelegate?
private let confirmationToastManager: CallControlsConfirmationToastManager
fileprivate var refreshView: (() -> Void)?
@MainActor
init(
call: SignalCall,
callService: CallService,
confirmationToastManager: CallControlsConfirmationToastManager,
delegate: CallControlsDelegate
) {
self.call = call
self.callService = callService
self.confirmationToastManager = confirmationToastManager
self.delegate = delegate
switch call.mode {
case .individual(let call):
call.addObserverAndSyncState(self)
case .groupThread(let call as GroupCall), .callLink(let call as GroupCall):
call.addObserver(self, syncStateImmediately: true)
}
callService.audioService.delegate = self
}
private var didOverrideDefaultMuteState = false
@MainActor
private var hasExternalAudioInputsAndAudioSource: Bool {
let audioService = callService.audioService
return audioService.hasExternalInputs && audioService.currentAudioSource != nil
}
@MainActor
var audioSourceButtonIsHidden: Bool {
if hasExternalAudioInputsAndAudioSource {
return false
} else if UIDevice.current.isIPad {
// iPad *only* supports speaker mode, if there are no external
// devices connected, so we don't need to show the button unless
// we have alternate audio sources.
return true
} else {
return !call.isOutgoingVideoMuted
}
}
struct AudioSourceButtonConfiguration {
let showDropdownArrow: Bool
let iconName: String
}
@MainActor
var audioSourceButtonConfiguration: AudioSourceButtonConfiguration {
let showDropdownArrow: Bool
let iconName: String
if
callService.audioService.hasExternalInputs,
let audioSource = callService.audioService.currentAudioSource
{
showDropdownArrow = true
if audioSource.isBuiltInEarPiece {
iconName = "phone-fill-28"
} else if audioSource.isBuiltInSpeaker {
iconName = "speaker-fill-28"
} else {
iconName = "speaker-bt-fill-28"
}
} else {
// No bluetooth audio detected
showDropdownArrow = false
iconName = "speaker-fill-28"
}
return AudioSourceButtonConfiguration(showDropdownArrow: showDropdownArrow, iconName: iconName)
}
var hangUpButtonIsHidden: Bool {
switch call.mode {
case .individual(_):
return false
case .groupThread(let call as GroupCall), .callLink(let call as GroupCall):
return !call.hasJoinedOrIsWaitingForAdminApproval
}
}
var muteButtonIsHidden: Bool {
return false
}
var videoButtonIsHidden: Bool {
return false
}
var flipCameraButtonIsHidden: Bool {
if call.isOutgoingVideoMuted {
return true
}
switch call.mode {
case .individual(let call):
return ![.idle, .dialing, .remoteRinging, .localRinging_Anticipatory, .localRinging_ReadyToAnswer].contains(call.state)
case .groupThread(let call as GroupCall), .callLink(let call as GroupCall):
if call.isJustMe {
return true
}
return call.hasJoinedOrIsWaitingForAdminApproval
}
}
var joinButtonIsHidden: Bool {
switch call.mode {
case .individual(_):
// TODO: Introduce lobby for starting 1:1 video calls.
return true
case .groupThread(let call as GroupCall), .callLink(let call as GroupCall):
return call.hasJoinedOrIsWaitingForAdminApproval
}
}
struct JoinButtonConfiguration {
let label: String
let color: UIColor
let adjustsImageWhenHighlighted: Bool
let isUserInteractionEnabled: Bool
}
@MainActor
var joinButtonConfig: JoinButtonConfiguration {
if call.isFull {
// Make the button look disabled, but don't actually disable it.
// We want to show a toast if the user taps anyway.
return JoinButtonConfiguration(
label: OWSLocalizedString(
"GROUP_CALL_IS_FULL",
comment: "Text explaining the group call is full"
),
color: .ows_whiteAlpha40,
adjustsImageWhenHighlighted: false,
isUserInteractionEnabled: true
)
}
if call.joinState == .joining {
return JoinButtonConfiguration(
label: "",
color: .ows_whiteAlpha40,
adjustsImageWhenHighlighted: false,
isUserInteractionEnabled: false
)
}
return JoinButtonConfiguration(
label: Self.joinButtonLabel(for: call),
color: .white,
adjustsImageWhenHighlighted: true,
isUserInteractionEnabled: true
)
}
@MainActor
static func joinButtonLabel(for call: SignalCall) -> String {
switch call.mode {
case .individual:
// We only show a lobby for 1:1 calls when the call is being initiated.
// TODO: The work of adding the lobby for 1:1 calls in the unified call view
// controller (currently GroupCallViewController) is not yet complete.
return startCallText()
case .groupThread(let call):
return call.ringRestrictions.contains(.callInProgress) ? CallStrings.joinGroupCall : startCallText()
case .callLink(let call):
return call.mayNeedToAskToJoin ? askToJoinText() : CallStrings.joinGroupCall
}
}
private static func startCallText() -> String {
return OWSLocalizedString(
"CALL_START_BUTTON",
comment: "Button to start a call"
)
}
private static func askToJoinText() -> String {
return OWSLocalizedString(
"ASK_TO_JOIN_CALL",
comment: "Button to try to join a call. The admin may need to approve the request before the user can join."
)
}
var shouldJoinButtonActivityIndicatorBeAnimating: Bool {
return (call.joinState == .joining || call.joinState == .pending) && !joinButtonIsHidden
}
@MainActor
var ringButtonIsHidden: Bool {
switch call.mode {
case .individual(_):
return true
case .groupThread(let call):
return call.joinState == .joined || call.ringRestrictions.contains(.callInProgress)
case .callLink:
return true
}
}
struct RingButtonConfiguration {
let isUserInteractionEnabled: Bool
let isSelected: Bool
let shouldDrawAsDisabled: Bool
}
@MainActor
var ringButtonConfiguration: RingButtonConfiguration? {
switch call.mode {
case .individual(_):
// We never show the ring button for 1:1 calls.
return nil
case .groupThread(let call):
// Leave the button visible but locked if joining, like the "join call" button.
let isUserInteractionEnabled = call.joinState == .notJoined
let isSelected: Bool
if
call.ringRestrictions.isEmpty,
case .shouldRing = call.groupCallRingState
{
isSelected = false
} else {
isSelected = true
}
// Leave the button enabled so we can present an explanatory toast, but show it disabled.
let shouldDrawAsDisabled = !call.ringRestrictions.isEmpty
return RingButtonConfiguration(
isUserInteractionEnabled: isUserInteractionEnabled,
isSelected: isSelected,
shouldDrawAsDisabled: shouldDrawAsDisabled
)
case .callLink:
return nil
}
}
var moreButtonIsHidden: Bool {
switch call.mode {
case .individual(_):
return true
case .groupThread(let call as GroupCall), .callLink(let call as GroupCall):
return call.ringRtcCall.localDeviceState.joinState != .joined
}
}
var videoButtonIsSelected: Bool {
return call.isOutgoingVideoMuted
}
var muteButtonIsSelected: Bool {
return call.isOutgoingAudioMuted
}
@MainActor
var ringButtonIsSelected: Bool {
if let config = ringButtonConfiguration {
return config.isSelected
}
// Ring button shouldn't be shown in this case anyway.
return false
}
@MainActor
var audioSourceButtonIsSelected: Bool {
return callService.audioService.isSpeakerEnabled
}
var flipCameraButtonIsSelected: Bool {
return false
}
var moreButtonIsSelected: Bool {
return false
}
func controlSpacing(controlCount: Int) -> CGFloat {
return (UIDevice.current.isNarrowerThanIPhone6 && controlCount > 4) ? 12 : 16
}
func shouldControlButtonsBeSmall(controlCount: Int) -> Bool {
return UIDevice.current.isIPad ? false : controlCount > 4
}
}
extension CallControlsViewModel: GroupCallObserver {
func groupCallLocalDeviceStateChanged(_ call: GroupCall) {
refreshView?()
}
func groupCallPeekChanged(_ call: GroupCall) {
// Mute if there's more than 8 people in the call.
if call.shouldMuteAutomatically(), !didOverrideDefaultMuteState, !muteButtonIsSelected {
callService.updateIsLocalAudioMuted(isLocalAudioMuted: true)
}
refreshView?()
}
func groupCallRemoteDeviceStatesChanged(_ call: GroupCall) {
refreshView?()
}
func groupCallEnded(_ call: GroupCall, reason: GroupCallEndReason) {
refreshView?()
}
}
extension CallControlsViewModel: IndividualCallObserver {
func individualCallStateDidChange(_ call: IndividualCall, state: CallState) {
refreshView?()
}
func individualCallLocalVideoMuteDidChange(_ call: IndividualCall, isVideoMuted: Bool) {
refreshView?()
}
func individualCallLocalAudioMuteDidChange(_ call: IndividualCall, isAudioMuted: Bool) {
refreshView?()
}
func individualCallHoldDidChange(_ call: IndividualCall, isOnHold: Bool) {
refreshView?()
}
func individualCallRemoteVideoMuteDidChange(_ call: IndividualCall, isVideoMuted: Bool) {
refreshView?()
}
func individualCallRemoteSharingScreenDidChange(_ call: IndividualCall, isRemoteSharingScreen: Bool) {
refreshView?()
}
}
extension CallControlsViewModel: CallAudioServiceDelegate {
func callAudioServiceDidChangeAudioSession(_ callAudioService: CallAudioService) {
refreshView?()
}
func callAudioServiceDidChangeAudioSource(_ callAudioService: CallAudioService, audioSource: AudioSource?) {
refreshView?()
}
}
extension CallControlsViewModel {
@MainActor
func didPressHangup() {
callService.callUIAdapter.localHangupCall(call)
delegate?.didPressHangup()
}
@MainActor
func didPressAudioSource() {
if callService.audioService.hasExternalInputs {
callService.audioService.presentRoutePicker()
} else {
let shouldEnableSpeakerphone = !audioSourceButtonIsSelected
callService.audioService.requestSpeakerphone(call: self.call, isEnabled: shouldEnableSpeakerphone)
confirmationToastManager.toastInducingCallControlChangeDidOccur(state: .speakerphone(isOn: shouldEnableSpeakerphone))
}
refreshView?()
}
@MainActor
func didPressMute() {
let shouldMute = !muteButtonIsSelected
callService.updateIsLocalAudioMuted(isLocalAudioMuted: shouldMute)
confirmationToastManager.toastInducingCallControlChangeDidOccur(state: .mute(isOn: shouldMute))
didOverrideDefaultMuteState = true
refreshView?()
}
@MainActor
func didPressVideo() {
callService.updateIsLocalVideoMuted(isLocalVideoMuted: !call.isOutgoingVideoMuted)
// When turning off video, default speakerphone to on.
if call.isOutgoingVideoMuted && !callService.audioService.hasExternalInputs {
callService.audioService.requestSpeakerphone(call: self.call, isEnabled: true)
}
refreshView?()
}
@MainActor
func didPressRing() {
switch call.mode {
case .individual:
owsFailDebug("Can't control ringing for an individual call.")
case .groupThread(let call):
if call.ringRestrictions.isEmpty {
switch call.groupCallRingState {
case .shouldRing:
call.groupCallRingState = .doNotRing
confirmationToastManager.toastInducingCallControlChangeDidOccur(state: .ring(isOn: false))
case .doNotRing:
call.groupCallRingState = .shouldRing
confirmationToastManager.toastInducingCallControlChangeDidOccur(state: .ring(isOn: true))
default:
owsFailBeta("Ring button should not have been available to press!")
}
refreshView?()
}
delegate?.didPressRing()
case .callLink:
owsFailDebug("Can't ring Call Link call.")
}
}
@MainActor
func didPressFlipCamera() {
if let isUsingFrontCamera = call.videoCaptureController.isUsingFrontCamera {
callService.updateCameraSource(call: call, isUsingFrontCamera: !isUsingFrontCamera)
refreshView?()
}
}
@objc
func didPressJoin() {
delegate?.didPressJoin()
}
@objc
func didPressMore() {
delegate?.didPressMore()
}
}
// MARK: - Accessibility
extension CallControlsViewModel {
public var hangUpButtonAccessibilityLabel: String {
switch call.mode {
case .individual(_):
return OWSLocalizedString(
"CALL_VIEW_HANGUP_LABEL",
comment: "Accessibility label for hang up call"
)
case .groupThread, .callLink:
return OWSLocalizedString(
"CALL_VIEW_LEAVE_CALL_LABEL",
comment: "Accessibility label for leaving a call"
)
}
}
public var audioSourceAccessibilityLabel: String {
// TODO: This is not the most helpful descriptor.
return OWSLocalizedString(
"CALL_VIEW_AUDIO_SOURCE_LABEL",
comment: "Accessibility label for selection the audio source"
)
}
public var muteButtonAccessibilityLabel: String {
if call.isOutgoingAudioMuted {
return OWSLocalizedString(
"CALL_VIEW_UNMUTE_LABEL",
comment: "Accessibility label for unmuting the microphone"
)
} else {
return OWSLocalizedString(
"CALL_VIEW_MUTE_LABEL",
comment: "Accessibility label for muting the microphone"
)
}
}
public var videoButtonAccessibilityLabel: String {
if call.isOutgoingVideoMuted {
return OWSLocalizedString(
"CALL_VIEW_TURN_VIDEO_ON_LABEL",
comment: "Accessibility label for turning on the camera"
)
} else {
return OWSLocalizedString(
"CALL_VIEW_TURN_VIDEO_OFF_LABEL",
comment: "Accessibility label for turning off the camera"
)
}
}
public var ringButtonAccessibilityLabel: String? {
switch call.mode {
case .individual:
owsFailDebug("Can't control ringing for an individual call.")
case .groupThread(let call):
switch call.groupCallRingState {
case .shouldRing:
return OWSLocalizedString(
"CALL_VIEW_TURN_OFF_RINGING",
comment: "Accessibility label for turning off call ringing"
)
case .doNotRing:
return OWSLocalizedString(
"CALL_VIEW_TURN_ON_RINGING",
comment: "Accessibility label for turning on call ringing"
)
default:
owsFailBeta("Ring button should not have been available to press!")
}
case .callLink:
owsFailDebug("Can't ring Call Link call.")
}
return nil
}
public var flipCameraButtonAccessibilityLabel: String {
return OWSLocalizedString(
"CALL_VIEW_SWITCH_CAMERA_DIRECTION",
comment: "Accessibility label to toggle front- vs. rear-facing camera"
)
}
public var moreButtonAccessibilityLabel: String {
return OWSLocalizedString(
"CALL_VIEW_MORE_LABEL",
comment: "Accessibility label for the More button in the Call Controls row."
)
}
}