From c2a76513f4bbf867cf9347fc11e51ef864b7dfd3 Mon Sep 17 00:00:00 2001 From: Marissa Le Coz <129999395+marissa-signal@users.noreply.github.com> Date: Thu, 30 Nov 2023 16:21:07 -0500 Subject: [PATCH] Extract delegate parts from GroupCallVC --- Signal/src/Calls/CallAudioService.swift | 2 +- .../Calls/UserInterface/CallControls.swift | 114 ++++++++++++-- .../Group/GroupCallViewController.swift | 145 +++++++----------- 3 files changed, 152 insertions(+), 109 deletions(-) diff --git a/Signal/src/Calls/CallAudioService.swift b/Signal/src/Calls/CallAudioService.swift index 37aaef555f..3b76cca274 100644 --- a/Signal/src/Calls/CallAudioService.swift +++ b/Signal/src/Calls/CallAudioService.swift @@ -27,7 +27,7 @@ class CallAudioService: NSObject, CallObserver { } // Track whether the speaker should be enabled or not. - private var isSpeakerEnabled = false + private(set) var isSpeakerEnabled = false // MARK: Vibration config private let vibrateRepeatDuration = 1.6 diff --git a/Signal/src/Calls/UserInterface/CallControls.swift b/Signal/src/Calls/UserInterface/CallControls.swift index 1c5a0eea35..689a54e713 100644 --- a/Signal/src/Calls/UserInterface/CallControls.swift +++ b/Signal/src/Calls/UserInterface/CallControls.swift @@ -9,13 +9,9 @@ import SignalUI @objc protocol CallControlsDelegate: AnyObject { - func didPressHangup(sender: UIButton) - func didPressAudioSource(sender: UIButton) - func didPressMute(sender: UIButton) - func didPressVideo(sender: UIButton) - func didPressRing(sender: UIButton) - func didPressFlipCamera(sender: UIButton) - func didPressJoin(sender: UIButton) + func didPressRing() + func didPressJoin() + func didPressHangup() } class CallControls: UIView { @@ -24,7 +20,7 @@ class CallControls: UIView { let button = createButton( iconName: "phone-down-fill-28", accessibilityLabel: viewModel.hangUpButtonAccessibilityLabel, - action: #selector(CallControlsDelegate.didPressHangup) + action: #selector(CallControlsViewModel.didPressHangup) ) button.unselectedBackgroundColor = .ows_accentRed return button @@ -32,28 +28,28 @@ class CallControls: UIView { private(set) lazy var audioSourceButton = createButton( iconName: "speaker-fill-28", accessibilityLabel: viewModel.audioSourceAccessibilityLabel, - action: #selector(CallControlsDelegate.didPressAudioSource) + action: #selector(CallControlsViewModel.didPressAudioSource) ) private lazy var muteButton = createButton( iconName: "mic-slash-fill-28", accessibilityLabel: viewModel.muteButtonAccessibilityLabel, - action: #selector(CallControlsDelegate.didPressMute) + action: #selector(CallControlsViewModel.didPressMute) ) private lazy var videoButton = createButton( iconName: "video-fill-28", accessibilityLabel: viewModel.videoButtonAccessibilityLabel, - action: #selector(CallControlsDelegate.didPressVideo) + action: #selector(CallControlsViewModel.didPressVideo) ) private lazy var ringButton = createButton( iconName: "bell-ring-fill-28", // TODO: Accessibility label - action: #selector(CallControlsDelegate.didPressRing) + action: #selector(CallControlsViewModel.didPressRing) ) private lazy var flipCameraButton: CallButton = { let button = createButton( iconName: "switch-camera-28", accessibilityLabel: viewModel.flipCameraButtonAccessibilityLabel, - action: #selector(CallControlsDelegate.didPressFlipCamera) + action: #selector(CallControlsViewModel.didPressFlipCamera) ) button.selectedIconColor = button.iconColor button.selectedBackgroundColor = button.unselectedBackgroundColor @@ -72,7 +68,7 @@ class CallControls: UIView { button.clipsToBounds = true button.layer.cornerRadius = height / 2 button.block = { [weak self, unowned button] in - self?.delegate.didPressJoin(sender: button) + self?.viewModel.didPressJoin() } button.contentEdgeInsets = UIEdgeInsets(top: 17, leading: 17, bottom: 17, trailing: 17) button.addSubview(joinButtonActivityIndicator) @@ -105,7 +101,8 @@ class CallControls: UIView { callService: CallService, delegate: CallControlsDelegate ) { - self.viewModel = CallControlsViewModel(call: call, callService: callService) + let viewModel = CallControlsViewModel(call: call, callService: callService, delegate: delegate) + self.viewModel = viewModel self.delegate = delegate super.init(frame: .zero) @@ -183,6 +180,9 @@ class CallControls: UIView { videoButton.isSelected = viewModel.videoButtonIsSelected muteButton.isSelected = viewModel.muteButtonIsSelected + audioSourceButton.isSelected = viewModel.audioSourceButtonIsSelected + ringButton.isSelected = viewModel.ringButtonIsSelected + flipCameraButton.isSelected = viewModel.flipCameraButtonIsSelected if !viewModel.audioSourceButtonIsHidden { let config = viewModel.audioSourceButtonConfiguration @@ -226,7 +226,7 @@ class CallControls: UIView { private func createButton(iconName: String, accessibilityLabel: String? = nil, action: Selector) -> CallButton { let button = CallButton(iconName: iconName) button.accessibilityLabel = accessibilityLabel - button.addTarget(delegate, action: action, for: .touchUpInside) + button.addTarget(viewModel, action: action, for: .touchUpInside) button.setContentHuggingHorizontalHigh() button.setCompressionResistanceHorizontalLow() button.alpha = 0.9 @@ -237,10 +237,12 @@ class CallControls: UIView { private class CallControlsViewModel { private let call: SignalCall private let callService: CallService + private weak var delegate: CallControlsDelegate? fileprivate var refreshView: (() -> Void)? - init(call: SignalCall, callService: CallService) { + init(call: SignalCall, callService: CallService, delegate: CallControlsDelegate) { self.call = call self.callService = callService + self.delegate = delegate call.addObserverAndSyncState(observer: self) callService.audioService.delegate = self } @@ -440,6 +442,27 @@ private class CallControlsViewModel { return call.isOutgoingAudioMuted } + var ringButtonIsSelected: Bool { + // Ring button currently only used for group calls. + switch call.groupCallRingState { + case .doNotRing: + return false + case .shouldRing: + return true + default: + // Ring button shouldn't be displayed in any other cases anyway + return false + } + } + + var audioSourceButtonIsSelected: Bool { + return callService.audioService.isSpeakerEnabled + } + + var flipCameraButtonIsSelected: Bool { + return false + } + func controlSpacing(controlCount: Int) -> CGFloat { return (UIDevice.current.isNarrowerThanIPhone6 && controlCount > 4) ? 12 : 16 } @@ -502,6 +525,63 @@ extension CallControlsViewModel: CallAudioServiceDelegate { } } +extension CallControlsViewModel { + @objc + func didPressHangup() { + callService.callUIAdapter.localHangupCall(call) + delegate?.didPressHangup() + } + + @objc + func didPressAudioSource() { + if callService.audioService.hasExternalInputs { + callService.audioService.presentRoutePicker() + } else { + callService.audioService.requestSpeakerphone(call: self.call, isEnabled: !audioSourceButtonIsSelected) + } + refreshView?() + } + + @objc + func didPressMute() { + callService.updateIsLocalAudioMuted(isLocalAudioMuted: !muteButtonIsSelected) + refreshView?() + } + + @objc + func didPressVideo() { + callService.updateIsLocalVideoMuted(isLocalVideoMuted: videoButtonIsSelected) + + // When turning off video, default speakerphone to on. + if !videoButtonIsSelected && !callService.audioService.hasExternalInputs { + callService.audioService.requestSpeakerphone(call: self.call, isEnabled: true) + } + refreshView?() + } + + @objc + func didPressRing() { + if call.ringRestrictions.isEmpty { + call.groupCallRingState = !ringButtonIsSelected ? .shouldRing : .doNotRing + refreshView?() + } + delegate?.didPressRing() + } + + @objc + func didPressFlipCamera() { + if let isUsingFrontCamera = call.videoCaptureController.isUsingFrontCamera { + callService.updateCameraSource(call: call, isUsingFrontCamera: !isUsingFrontCamera) + refreshView?() + } + } + + @objc + func didPressJoin() { + delegate?.didPressJoin() + } +} + // MARK: - Accessibility extension CallControlsViewModel { diff --git a/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift b/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift index 492559f843..0194be45be 100644 --- a/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift +++ b/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift @@ -539,11 +539,14 @@ class GroupCallViewController: UIViewController { updateSwipeToastView() } - func dismissCall(shouldHangUp: Bool = true) { + private func dismissCall(shouldHangUp: Bool = true) { if shouldHangUp { callService.callUIAdapter.localHangupCall(call) } + didHangupCall() + } + func didHangupCall() { guard !hasDismissed else { return } @@ -862,96 +865,6 @@ extension GroupCallViewController: CallObserver { } } -extension GroupCallViewController: CallControlsDelegate { - func didPressHangup(sender: UIButton) { - dismissCall() - } - - func didPressAudioSource(sender: UIButton) { - if callService.audioService.hasExternalInputs { - callService.audioService.presentRoutePicker() - } else { - sender.isSelected = !sender.isSelected - callService.audioService.requestSpeakerphone(call: self.call, isEnabled: sender.isSelected) - } - } - - func didPressMute(sender: UIButton) { - sender.isSelected = !sender.isSelected - callService.updateIsLocalAudioMuted(isLocalAudioMuted: sender.isSelected) - } - - func didPressVideo(sender: UIButton) { - sender.isSelected = !sender.isSelected - - callService.updateIsLocalVideoMuted(isLocalVideoMuted: !sender.isSelected) - - // When turning off video, default speakerphone to on. - if !sender.isSelected && !callService.audioService.hasExternalInputs { - callControls.audioSourceButton.isSelected = true - callService.audioService.requestSpeakerphone(call: self.call, isEnabled: true) - } - } - - func didPressRing(sender: UIButton) { - if call.ringRestrictions.isEmpty { - let oldShouldRing = sender.isSelected - let newShouldRing = !oldShouldRing - sender.isSelected = newShouldRing - call.groupCallRingState = newShouldRing ? .shouldRing : .doNotRing - - // Refresh the call header. - callHeader.groupCallLocalDeviceStateChanged(call) - } else { - if call.ringRestrictions.intersects([.notApplicable, .callInProgress]) { - owsFailDebug("should not show the ring button at all") - } else if call.ringRestrictions.contains(.groupTooLarge) { - let toast = ToastController(text: OWSLocalizedString("GROUP_CALL_TOO_LARGE_TO_RING", comment: "Text displayed when trying to turn on ringing when calling a large group.")) - toast.presentToastView(from: .top, of: view, inset: view.safeAreaInsets.top + 8) - } else { - owsAssertDebug(call.ringRestrictions.isEmpty, "unknown ring restriction") - } - } - } - - func didPressFlipCamera(sender: UIButton) { - sender.isSelected = !sender.isSelected - callService.updateCameraSource(call: call, isUsingFrontCamera: !sender.isSelected) - } - - func didPressJoin(sender: UIButton) { - guard !call.groupCall.isFull else { - let text: String - if let maxDevices = call.groupCall.maxDevices { - let formatString = OWSLocalizedString("GROUP_CALL_HAS_MAX_DEVICES_%d", tableName: "PluralAware", - comment: "An error displayed to the user when the group call ends because it has exceeded the max devices. Embeds {{max device count}}." - ) - text = String.localizedStringWithFormat(formatString, maxDevices) - } else { - text = OWSLocalizedString( - "GROUP_CALL_HAS_MAX_DEVICES_UNKNOWN_COUNT", - comment: "An error displayed to the user when the group call ends because it has exceeded the max devices." - ) - } - - let toastController = ToastController(text: text) - // Leave the toast up longer than usual because this message is pretty long. - toastController.presentToastView(from: .top, - of: view, - inset: view.safeAreaInsets.top + 8, - dismissAfter: .seconds(8)) - return - } - - presentSafetyNumberChangeSheetIfNecessary { [weak self] success in - guard let self = self else { return } - if success { - self.callService.joinGroupCallIfNecessary(self.call) - } - } - } -} - extension GroupCallViewController: IncomingCallControlsDelegate { func didDeclineIncomingCall() { dismissCall() @@ -1014,6 +927,56 @@ extension GroupCallViewController: UIScrollViewDelegate { } } +extension GroupCallViewController: CallControlsDelegate { + func didPressRing() { + if call.ringRestrictions.isEmpty { + // Refresh the call header. + callHeader.groupCallLocalDeviceStateChanged(call) + } else if call.ringRestrictions.contains(.groupTooLarge) { + let toast = ToastController(text: OWSLocalizedString("GROUP_CALL_TOO_LARGE_TO_RING", comment: "Text displayed when trying to turn on ringing when calling a large group.")) + toast.presentToastView(from: .top, of: view, inset: view.safeAreaInsets.top + 8) + } + } + + func didPressJoin() { + guard call.canJoin else { + let text: String + if let maxDevices = call.groupCall.maxDevices { + let formatString = OWSLocalizedString("GROUP_CALL_HAS_MAX_DEVICES_%d", tableName: "PluralAware", + comment: "An error displayed to the user when the group call ends because it has exceeded the max devices. Embeds {{max device count}}." + ) + text = String.localizedStringWithFormat(formatString, maxDevices) + } else { + text = OWSLocalizedString( + "GROUP_CALL_HAS_MAX_DEVICES_UNKNOWN_COUNT", + comment: "An error displayed to the user when the group call ends because it has exceeded the max devices." + ) + } + + let toastController = ToastController(text: text) + // Leave the toast up longer than usual because this message is pretty long. + toastController.presentToastView( + from: .top, + of: view, + inset: view.safeAreaInsets.top + 8, + dismissAfter: .seconds(8) + ) + return + } + + presentSafetyNumberChangeSheetIfNecessary { [weak self] success in + guard let self = self else { return } + if success { + self.callService.joinGroupCallIfNecessary(self.call) + } + } + } + + func didPressHangup() { + didHangupCall() + } +} + extension GroupCallViewController: GroupCallMemberViewDelegate { func memberView(_ view: GroupCallMemberView, userRequestedInfoAboutError error: GroupCallMemberView.ErrorState) { let title: String