From 320d7e2c72b48a44e4efcf8d368234fdeec4c9aa Mon Sep 17 00:00:00 2001 From: Marissa Le Coz <129999395+marissa-signal@users.noreply.github.com> Date: Wed, 3 Jan 2024 14:46:40 -0500 Subject: [PATCH] Call controls confirmation toasts --- Signal.xcodeproj/project.pbxproj | 4 + .../mic/mic-slash.imageset/Contents.json | 15 ++ .../mic/mic-slash.imageset/mic-slash.pdf | 146 +++++++++++++ .../speaker-slash.imageset/Contents.json | 15 ++ .../speaker-slash.imageset/speaker-slash.pdf | 110 ++++++++++ .../Calls/UserInterface/CallControls.swift | 27 ++- .../CallControlsConfirmationToast.swift | 198 ++++++++++++++++++ .../Group/GroupCallViewController.swift | 22 +- .../IndividualCallViewController.swift | 13 ++ .../translations/en.lproj/Localizable.strings | 18 ++ 10 files changed, 562 insertions(+), 6 deletions(-) create mode 100644 Signal/Symbols.xcassets/mic/mic-slash.imageset/Contents.json create mode 100644 Signal/Symbols.xcassets/mic/mic-slash.imageset/mic-slash.pdf create mode 100644 Signal/Symbols.xcassets/speaker/speaker-slash.imageset/Contents.json create mode 100644 Signal/Symbols.xcassets/speaker/speaker-slash.imageset/speaker-slash.pdf create mode 100644 Signal/src/Calls/UserInterface/CallControlsConfirmationToast.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index d0309cda3f..e2018b3421 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -1581,6 +1581,7 @@ D9FC835029F71CC300878BC8 /* GroupUpdateInfoMessageInserter+PrecomputedUpdateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9FC834F29F71CC300878BC8 /* GroupUpdateInfoMessageInserter+PrecomputedUpdateType.swift */; }; E1368CBE18A1C36B00109378 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.framework */; }; E14EDF6E2A71AFDF00F0FD7C /* RecipientContextMenuHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14EDF6D2A71AFDF00F0FD7C /* RecipientContextMenuHelper.swift */; }; + E17283DE2B2A713C00302DC7 /* CallControlsConfirmationToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17283DD2B2A713C00302DC7 /* CallControlsConfirmationToast.swift */; }; E18C4A7729EF2ECC007534D4 /* SignalAccountTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18C4A7629EF2ECC007534D4 /* SignalAccountTest.swift */; }; E1A090382A4B909B00F2BE8B /* RecipientHidingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A090372A4B909B00F2BE8B /* RecipientHidingManager.swift */; }; E1C2A54B2A8FCB0D00AEC4DA /* DeleteSystemContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C2A54A2A8FCB0D00AEC4DA /* DeleteSystemContactViewController.swift */; }; @@ -4313,6 +4314,7 @@ DE2DD605305BC6EFAD731723 /* Pods-Signal.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.debug.xcconfig"; sourceTree = ""; }; DF728B4B438716EAF95CEC18 /* Pods-Signal.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.app store release.xcconfig"; sourceTree = ""; }; E14EDF6D2A71AFDF00F0FD7C /* RecipientContextMenuHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientContextMenuHelper.swift; sourceTree = ""; }; + E17283DD2B2A713C00302DC7 /* CallControlsConfirmationToast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallControlsConfirmationToast.swift; sourceTree = ""; }; E18C4A7629EF2ECC007534D4 /* SignalAccountTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalAccountTest.swift; sourceTree = ""; }; E1A090372A4B909B00F2BE8B /* RecipientHidingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientHidingManager.swift; sourceTree = ""; }; E1A0AD8B16E13FDD0071E604 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; @@ -8062,6 +8064,7 @@ 88588D27252E5A1F00405414 /* Individual */, 88238EA324E9DDE900F28079 /* CallButton.swift */, 88588D1A252D3DFF00405414 /* CallControls.swift */, + E17283DD2B2A713C00302DC7 /* CallControlsConfirmationToast.swift */, 88ABB8B42534070400229EAA /* CallHeader.swift */, 88D23D1C23CEC0C700B0E74B /* CallUIAdapter.swift */, 1700E33E28B856FB0073D949 /* IncomingCallControls.swift */, @@ -12029,6 +12032,7 @@ 88D23D2C23CEC17400B0E74B /* CallAudioService.swift in Sources */, 88238EA424E9DDE900F28079 /* CallButton.swift in Sources */, 88588D1B252D3DFF00405414 /* CallControls.swift in Sources */, + E17283DE2B2A713C00302DC7 /* CallControlsConfirmationToast.swift in Sources */, 88ABB8B52534070400229EAA /* CallHeader.swift in Sources */, 88D23D2423CEC0C700B0E74B /* CallKitCallManager.swift in Sources */, 88D23D2523CEC0C700B0E74B /* CallKitCallUIAdaptee.swift in Sources */, diff --git a/Signal/Symbols.xcassets/mic/mic-slash.imageset/Contents.json b/Signal/Symbols.xcassets/mic/mic-slash.imageset/Contents.json new file mode 100644 index 0000000000..6d473db8c0 --- /dev/null +++ b/Signal/Symbols.xcassets/mic/mic-slash.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "mic-slash.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Signal/Symbols.xcassets/mic/mic-slash.imageset/mic-slash.pdf b/Signal/Symbols.xcassets/mic/mic-slash.imageset/mic-slash.pdf new file mode 100644 index 0000000000..5d920918f1 --- /dev/null +++ b/Signal/Symbols.xcassets/mic/mic-slash.imageset/mic-slash.pdf @@ -0,0 +1,146 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.625000 10.791534 cm +0.000000 0.000000 0.000000 scn +4.869258 9.072965 m +6.282362 7.659862 l +6.501116 9.171856 7.802404 10.333466 9.375000 10.333466 c +11.100889 10.333466 12.500000 8.934355 12.500000 7.208466 c +12.500000 1.708466 l +12.500000 1.622302 12.496512 1.536952 12.489672 1.452551 c +13.942234 -0.000010 l +14.141206 0.531660 14.250000 1.107358 14.250000 1.708466 c +14.250000 7.208466 l +14.250000 9.900854 12.067389 12.083466 9.375000 12.083466 c +7.343011 12.083466 5.601388 10.840257 4.869258 9.072965 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 2.625000 16.983765 cm +0.000000 0.000000 0.000000 scn +10.303020 -9.270542 m +8.523958 -7.491481 l +7.482240 -7.197309 6.661456 -6.376525 6.367284 -5.334806 c +4.500000 -3.467522 l +4.500000 -4.483765 l +4.500000 -7.176152 6.682612 -9.358765 9.375000 -9.358765 c +9.692335 -9.358765 10.002587 -9.328444 10.303020 -9.270542 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 2.625000 9.625000 cm +0.000000 0.000000 0.000000 scn +11.715831 -3.324589 m +13.045951 -4.654709 l +12.186229 -5.074633 11.243936 -5.351652 10.250000 -5.454840 c +10.250000 -6.750000 l +13.625000 -6.750000 l +14.108249 -6.750000 14.500000 -7.141750 14.500000 -7.625000 c +14.500000 -8.108250 14.108249 -8.500000 13.625000 -8.500000 c +5.125000 -8.500000 l +4.641751 -8.500000 4.250000 -8.108250 4.250000 -7.625000 c +4.250000 -7.141750 4.641751 -6.750000 5.125000 -6.750000 c +8.500000 -6.750000 l +8.500000 -5.454840 l +4.285833 -5.017340 1.000000 -1.454895 1.000000 2.875000 c +1.000000 3.875000 l +1.000000 4.358249 1.391751 4.750000 1.875000 4.750000 c +2.358249 4.750000 2.750000 4.358249 2.750000 3.875000 c +2.750000 2.875000 l +2.750000 -0.783886 5.716114 -3.750000 9.375000 -3.750000 c +10.199077 -3.750000 10.988011 -3.599539 11.715831 -3.324589 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 2.625000 16.681549 cm +0.000000 0.000000 0.000000 scn +16.552219 -8.500011 m +15.266595 -7.214387 l +15.735333 -6.305653 16.000000 -5.274509 16.000000 -4.181549 c +16.000000 -3.181549 l +16.000000 -2.698300 16.391750 -2.306549 16.875000 -2.306549 c +17.358250 -2.306549 17.750000 -2.698300 17.750000 -3.181549 c +17.750000 -4.181549 l +17.750000 -5.761374 17.312569 -7.239032 16.552219 -8.500011 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 2.625000 3.954163 cm +0.000000 0.000000 0.000000 scn +1.493718 17.164557 m +1.152010 17.506264 0.597990 17.506264 0.256282 17.164557 c +-0.085427 16.822847 -0.085427 16.268827 0.256282 15.927119 c +17.256281 -1.072880 l +17.597990 -1.414589 18.152010 -1.414589 18.493717 -1.072880 c +18.835426 -0.731173 18.835426 -0.177153 18.493717 0.164557 c +1.493718 17.164557 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 2677 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002767 00000 n +0000002790 00000 n +0000002963 00000 n +0000003037 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3096 +%%EOF \ No newline at end of file diff --git a/Signal/Symbols.xcassets/speaker/speaker-slash.imageset/Contents.json b/Signal/Symbols.xcassets/speaker/speaker-slash.imageset/Contents.json new file mode 100644 index 0000000000..cfc21d2efa --- /dev/null +++ b/Signal/Symbols.xcassets/speaker/speaker-slash.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "speaker-slash.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Signal/Symbols.xcassets/speaker/speaker-slash.imageset/speaker-slash.pdf b/Signal/Symbols.xcassets/speaker/speaker-slash.imageset/speaker-slash.pdf new file mode 100644 index 0000000000..3e1568124b --- /dev/null +++ b/Signal/Symbols.xcassets/speaker/speaker-slash.imageset/speaker-slash.pdf @@ -0,0 +1,110 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.625000 2.954102 cm +0.000000 0.000000 0.000000 scn +1.493718 17.664618 m +1.152010 18.006325 0.597990 18.006325 0.256282 17.664618 c +-0.085427 17.322908 -0.085427 16.768888 0.256282 16.427179 c +16.256281 0.427179 l +16.597990 0.085470 17.152010 0.085470 17.493719 0.427179 c +17.835428 0.768888 17.835428 1.322906 17.493719 1.664616 c +1.493718 17.664618 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 3.625000 7.786987 cm +0.000000 0.000000 0.000000 scn +1.000000 6.213000 m +1.000000 6.915573 1.252011 7.559285 1.670569 8.058672 c +2.920504 6.808736 l +2.812453 6.635981 2.750000 6.431784 2.750000 6.213000 c +2.750000 2.213000 l +2.750000 1.591680 3.253680 1.088000 3.875000 1.088000 c +7.737508 1.088000 l +11.500001 -2.674492 l +11.500001 -1.770761 l +13.182144 -3.452904 l +12.850535 -4.569654 11.379924 -5.029287 10.475953 -4.125317 c +7.012634 -0.662000 l +3.875000 -0.662000 l +2.287181 -0.662000 1.000000 0.625181 1.000000 2.213000 c +1.000000 6.213000 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 3.625000 10.496582 cm +0.000000 0.000000 0.000000 scn +8.423059 5.314100 m +11.500000 8.391040 l +11.500000 2.237160 l +13.250001 0.487160 l +13.250000 8.692817 l +13.250000 10.140533 11.499649 10.865561 10.475953 9.841866 c +7.185623 6.551537 l +8.423059 5.314100 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1327 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001417 00000 n +0000001440 00000 n +0000001613 00000 n +0000001687 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1746 +%%EOF \ No newline at end of file diff --git a/Signal/src/Calls/UserInterface/CallControls.swift b/Signal/src/Calls/UserInterface/CallControls.swift index e40c4cc4d0..5ef9cf780c 100644 --- a/Signal/src/Calls/UserInterface/CallControls.swift +++ b/Signal/src/Calls/UserInterface/CallControls.swift @@ -102,9 +102,15 @@ class CallControls: UIView { init( call: SignalCall, callService: CallService, + confirmationToastManager: CallControlsConfirmationToastManager, delegate: CallControlsDelegate ) { - let viewModel = CallControlsViewModel(call: call, callService: callService, delegate: delegate) + let viewModel = CallControlsViewModel( + call: call, + callService: callService, + confirmationToastManager: confirmationToastManager, + delegate: delegate + ) self.viewModel = viewModel self.delegate = delegate super.init(frame: .zero) @@ -247,10 +253,17 @@ private class CallControlsViewModel { private let call: SignalCall private let callService: CallService private weak var delegate: CallControlsDelegate? + private let confirmationToastManager: CallControlsConfirmationToastManager fileprivate var refreshView: (() -> Void)? - init(call: SignalCall, callService: CallService, delegate: CallControlsDelegate) { + init( + call: SignalCall, + callService: CallService, + confirmationToastManager: CallControlsConfirmationToastManager, + delegate: CallControlsDelegate + ) { self.call = call self.callService = callService + self.confirmationToastManager = confirmationToastManager self.delegate = delegate call.addObserverAndSyncState(observer: self) callService.audioService.delegate = self @@ -541,14 +554,18 @@ extension CallControlsViewModel { if callService.audioService.hasExternalInputs { callService.audioService.presentRoutePicker() } else { - callService.audioService.requestSpeakerphone(call: self.call, isEnabled: !audioSourceButtonIsSelected) + let shouldEnableSpeakerphone = !audioSourceButtonIsSelected + callService.audioService.requestSpeakerphone(call: self.call, isEnabled: shouldEnableSpeakerphone) + confirmationToastManager.toastInducingCallControlChangeDidOccur(state: .speakerphone(isOn: shouldEnableSpeakerphone)) } refreshView?() } @objc func didPressMute() { - callService.updateIsLocalAudioMuted(isLocalAudioMuted: !muteButtonIsSelected) + let shouldMute = !muteButtonIsSelected + callService.updateIsLocalAudioMuted(isLocalAudioMuted: shouldMute) + confirmationToastManager.toastInducingCallControlChangeDidOccur(state: .mute(isOn: shouldMute)) refreshView?() } @@ -569,8 +586,10 @@ extension CallControlsViewModel { 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!") } diff --git a/Signal/src/Calls/UserInterface/CallControlsConfirmationToast.swift b/Signal/src/Calls/UserInterface/CallControlsConfirmationToast.swift new file mode 100644 index 0000000000..5604511e3e --- /dev/null +++ b/Signal/src/Calls/UserInterface/CallControlsConfirmationToast.swift @@ -0,0 +1,198 @@ +// +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import SignalMessaging + +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 = UIViewPropertyAnimator( + duration: 0.2, + springDamping: 0.8, + springResponse: 0.2 + ) + disappearAnimator.addAnimations { + toast.alpha = 0 + } + disappearAnimator.addCompletion { _ in + toast.removeFromSuperview() + } + disappearAnimator.startAnimation(afterDelay: 2) + } + appearAnimator.startAnimation() + } +} diff --git a/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift b/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift index 0194be45be..4d8bdbb9fa 100644 --- a/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift +++ b/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift @@ -14,7 +14,16 @@ import SignalUI class GroupCallViewController: UIViewController { private let call: SignalCall private var groupCall: GroupCall { call.groupCall } - private lazy var callControls = CallControls(call: call, callService: callService, delegate: self) + private lazy var callControlsConfirmationToastManager = CallControlsConfirmationToastManager( + presentingContainerView: callControlsConfirmationToastContainerView + ) + private lazy var callControls = CallControls( + call: call, + callService: callService, + confirmationToastManager: callControlsConfirmationToastManager, + delegate: self + ) + private lazy var callControlsConfirmationToastContainerView = UIView() private lazy var incomingCallControls = IncomingCallControls(video: true, delegate: self) private var incomingCallControlsConstraint: NSLayoutConstraint? private lazy var noVideoIndicatorView: UIStackView = createNoVideoIndicatorView() @@ -217,6 +226,15 @@ class GroupCallViewController: UIViewController { swipeToastView.autoPinEdge(toSuperviewMargin: .leading, relation: .greaterThanOrEqual) swipeToastView.autoPinEdge(toSuperviewMargin: .trailing, relation: .greaterThanOrEqual) + // Confirmation toasts should sit on top of the `localMemberView` and `videoOverflow`, + // so this `addSubview` should remain towards the end of the setup. + // + // TODO: The call controls '...' overflow menu (which will include reactions and raise hand + // options) will sit on top of these toasts. + view.addSubview(callControlsConfirmationToastContainerView) + callControlsConfirmationToastContainerView.autoPinEdge(.bottom, to: .top, of: callControls, withOffset: -30) + callControlsConfirmationToastContainerView.autoHCenterInSuperview() + view.addGestureRecognizer(tapGesture) updateCallUI() @@ -382,7 +400,7 @@ class GroupCallViewController: UIViewController { speakerPage.addSubview(speakerView) speakerView.autoPinEdgesToSuperviewEdges() - view.addSubview(localMemberView) + view.insertSubview(localMemberView, belowSubview: callControlsConfirmationToastContainerView) if groupCall.remoteDeviceStates.count > 1 { let pipWidth = GroupCallVideoOverflow.itemHeight * ReturnToCallViewController.pipSize.aspectRatio diff --git a/Signal/src/Calls/UserInterface/Individual/IndividualCallViewController.swift b/Signal/src/Calls/UserInterface/Individual/IndividualCallViewController.swift index 10e5d9b706..f995b26747 100644 --- a/Signal/src/Calls/UserInterface/Individual/IndividualCallViewController.swift +++ b/Signal/src/Calls/UserInterface/Individual/IndividualCallViewController.swift @@ -32,9 +32,14 @@ class IndividualCallViewController: OWSViewController, CallObserver { private var callDurationTimer: Timer? + private lazy var callControlsConfirmationToastManager = CallControlsConfirmationToastManager( + presentingContainerView: callControlsConfirmationToastContainerView + ) + private lazy var callControlsConfirmationToastContainerView = UIView() private lazy var callControls = CallControls( call: call, callService: callService, + confirmationToastManager: callControlsConfirmationToastManager, delegate: self ) @@ -275,6 +280,13 @@ class IndividualCallViewController: OWSViewController, CallObserver { callControls.autoPinEdge(toSuperviewEdge: .leading) callControls.autoPinEdge(toSuperviewEdge: .trailing) + // Confirmation toasts should sit on top of the `localVideoView` + // and most other UI elements, so this `addSubview` should remain towards + // the end of the setup. + view.addSubview(callControlsConfirmationToastContainerView) + callControlsConfirmationToastContainerView.autoPinEdge(.bottom, to: .top, of: callControls, withOffset: -30) + callControlsConfirmationToastContainerView.autoHCenterInSuperview() + createContactViews() createIncomingCallControls() } @@ -512,6 +524,7 @@ class IndividualCallViewController: OWSViewController, CallObserver { guard !localVideoView.isHidden else { return } view.bringSubviewToFront(localVideoView) + view.bringSubviewToFront(callControlsConfirmationToastContainerView) let pipSize = ReturnToCallViewController.pipSize let lastBoundingRect = lastLocalVideoBoundingRect diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index b2887801bf..0a5d9079c3 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -4192,6 +4192,9 @@ /* A recovery suggestion shown as the body of an alert when try to link a new device & the user is already at the limit. */ "MULTIDEVICE_PAIRING_MAX_RECOVERY" = "You have reached the maximum of devices you can currently link with your account. Please remove a device and try again."; +/* Text for a toast confirming that the mic has been muted for a call. */ +"MUTE_CONFIRMATION_TOAST_LABEL" = "Mic off"; + /* Badge indicating that the user is muted. */ "MUTED_BADGE" = "Muted"; @@ -5689,6 +5692,12 @@ /* Button to return to current group call */ "RETURN_CALL_PILL_BUTTON" = "Return"; +/* Text for a toast confirming that ringing has been turned off for a call. */ +"RING_OFF_CONFIRMATION_TOAST_LABEL" = "Ringing off"; + +/* Text for a toast confirming that ringing has been turned on for a call. */ +"RING_ON_CONFIRMATION_TOAST_LABEL" = "Ringing on"; + /* button title to confirm adding a recipient when their safety number has recently changed */ "SAFETY_NUMBER_CHANGED_CONFIRM_ADD_MEMBER_ACTION" = "Add Anyway"; @@ -6937,6 +6946,12 @@ /* Title for the captcha view controller */ "SPAM_CAPTCHA_VIEW_CONTROLLER" = "Complete verification"; +/* Text for a toast confirming that the speakerphone has been turned off for a call. */ +"SPEAKERPHONE_OFF_CONFIRMATION_TOAST_LABEL" = "Speaker off"; + +/* Text for a toast confirming that the speakerphone has been turned on for a call. */ +"SPEAKERPHONE_ON_CONFIRMATION_TOAST_LABEL" = "Speaker on"; + /* The name for the sticker category 'Featured' */ "STICKER_CATEGORY_FEATURED_NAME" = "Featured"; @@ -7588,6 +7603,9 @@ /* Alert title when unlinking device fails */ "UNLINKING_FAILED_ALERT_TITLE" = "Signal was unable to unlink your device."; +/* Text for a toast confirming that the mic has been unmuted for a call. */ +"UNMUTE_CONFIRMATION_TOAST_LABEL" = "Mic on"; + /* Pressing this button un-pins a thread */ "UNPIN_ACTION" = "Unpin";