diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallActivity.kt index c7e3aba5a8..c8e463b122 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallActivity.kt @@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet import org.thoughtcrime.securesms.util.FullscreenHelper import org.thoughtcrime.securesms.util.VibrateUtil import org.thoughtcrime.securesms.util.viewModel @@ -101,6 +102,8 @@ class CallActivity : BaseActivity(), CallControlsCallback { viewModel.callActions.collect { when (it) { CallViewModel.Action.EnableVideo -> onVideoToggleClick(true) + is CallViewModel.Action.ShowGroupCallSafetyNumberChangeDialog -> SafetyNumberBottomSheet.forGroupCall(it.untrustedIdentities).show(supportFragmentManager) + CallViewModel.Action.SwitchToSpeaker -> Unit // TODO - Switch user to speaker view. } } } @@ -324,6 +327,10 @@ class CallActivity : BaseActivity(), CallControlsCallback { viewModel.hangup() } + override fun onVideoTooltipDismissed() { + viewModel.onVideoTooltipDismissed() + } + private fun observeCallEvents() { webRtcCallViewModel.events.observe(this) { event -> viewModel.onCallEvent(event) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallControls.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallControls.kt index 314263da10..942ea732d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallControls.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallControls.kt @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.util.RemoteConfig */ @Composable fun CallControls( + displayVideoTooltip: Boolean, callControlsState: CallControlsState, callControlsCallback: CallControlsCallback, modifier: Modifier = Modifier @@ -96,10 +97,16 @@ fun CallControls( val hasCameraPermission = ContextCompat.checkSelfPermission(LocalContext.current, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED if (callControlsState.displayVideoToggle) { - ToggleVideoButton( - isVideoEnabled = callControlsState.isVideoEnabled && hasCameraPermission, - onChange = callControlsCallback::onVideoToggleClick - ) + CallScreenTooltipBox( + text = stringResource(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video), + displayTooltip = displayVideoTooltip, + onTooltipDismissed = callControlsCallback::onVideoTooltipDismissed + ) { + ToggleVideoButton( + isVideoEnabled = callControlsState.isVideoEnabled && hasCameraPermission, + onChange = callControlsCallback::onVideoToggleClick + ) + } } val hasRecordAudioPermission = ContextCompat.checkSelfPermission(LocalContext.current, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED @@ -162,6 +169,7 @@ fun CallControlsPreview() { startCallButtonText = R.string.WebRtcCallView__start_call, displayEndCallButton = true ), + displayVideoTooltip = false, callControlsCallback = CallControlsCallback.Empty ) } @@ -179,6 +187,7 @@ interface CallControlsCallback { fun onAdditionalActionsClick() fun onStartCallClick(isVideoCall: Boolean) fun onEndCallClick() + fun onVideoTooltipDismissed() object Empty : CallControlsCallback { override fun onAudioDeviceSheetDisplayChanged(displayed: Boolean) = Unit @@ -189,6 +198,7 @@ interface CallControlsCallback { override fun onAdditionalActionsClick() = Unit override fun onStartCallClick(isVideoCall: Boolean) = Unit override fun onEndCallClick() = Unit + override fun onVideoTooltipDismissed() = Unit } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt index 9f3015e6ad..0115efbd3e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt @@ -154,6 +154,7 @@ fun CallScreen( CallControls( callControlsState = callControlsState, callControlsCallback = callControlsCallback, + displayVideoTooltip = callScreenState.displayVideoTooltip, modifier = Modifier .fillMaxWidth() .alpha(callControlsAlpha) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenState.kt index 6dd01ee285..d88a618838 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenState.kt @@ -25,7 +25,11 @@ data class CallScreenState( val hangup: Hangup? = null, val callControlsChange: CallControlsChange? = null, val callStatus: CallString? = null, - val isDisplayingAudioToggleSheet: Boolean = false + val isDisplayingAudioToggleSheet: Boolean = false, + val displaySwitchCameraTooltip: Boolean = false, + val displayVideoTooltip: Boolean = false, + val displaySwipeToSpeakerHint: Boolean = false, + val displayWifiToCellularPopup: Boolean = false ) { data class Hangup( val hangupMessageType: HangupMessage.Type, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenTooltipBox.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenTooltipBox.kt new file mode 100644 index 0000000000..3ea9613e50 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenTooltipBox.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc.v2 + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.Text +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.rememberTooltipState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import kotlinx.coroutines.flow.drop +import org.signal.core.ui.DarkPreview +import org.signal.core.ui.Previews +import org.thoughtcrime.securesms.R + +/** + * Tooltip box appropriately styled for the call screen. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CallScreenTooltipBox( + text: String, + displayTooltip: Boolean, + onTooltipDismissed: () -> Unit = {}, + content: @Composable () -> Unit +) { + val state = rememberTooltipState() + + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + state = state, + tooltip = { + PlainTooltip( + caretSize = TooltipDefaults.caretSize, + shape = TooltipDefaults.plainTooltipContainerShape, + containerColor = colorResource(R.color.signal_light_colorPrimary), + contentColor = colorResource(R.color.signal_light_colorOnPrimary) + ) { + Text(text = text) + } + }, + content = content + ) + + LaunchedEffect(displayTooltip) { + if (displayTooltip) { + state.show() + } else { + state.dismiss() + } + } + + LaunchedEffect(state) { + snapshotFlow { state.isVisible } + .drop(1) + .collect { onTooltipDismissed() } + } +} + +@DarkPreview +@Composable +fun SwitchCameraTooltipBoxPreview() { + Previews.Preview { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + CallScreenTooltipBox( + text = "Test Tooltip", + displayTooltip = true + ) { + Text(text = "Test Content") + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallViewModel.kt index 0f9d5de83a..d767445edb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallViewModel.kt @@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel +import org.thoughtcrime.securesms.database.model.IdentityRecord import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.events.WebRtcViewModel import org.thoughtcrime.securesms.recipients.Recipient @@ -144,17 +145,30 @@ class CallViewModel( internalDialog.update { CallScreenDialogType.NONE } } + fun onVideoTooltipDismissed() { + webRtcCallViewModel.onDismissedVideoTooltip() + internalCallScreenState.update { it.copy(displayVideoTooltip = false) } + } + fun onCallEvent(event: CallEvent) { when (event) { - CallEvent.DismissSwitchCameraTooltip -> Unit // TODO - CallEvent.DismissVideoTooltip -> Unit // TODO - is CallEvent.ShowGroupCallSafetyNumberChange -> Unit // TODO - CallEvent.ShowSwipeToSpeakerHint -> Unit // TODO - CallEvent.ShowSwitchCameraTooltip -> Unit // TODO - CallEvent.ShowVideoTooltip -> Unit // TODO - CallEvent.ShowWifiToCellularPopup -> Unit // TODO + CallEvent.DismissSwitchCameraTooltip -> internalCallScreenState.update { it.copy(displaySwitchCameraTooltip = false) } + CallEvent.DismissVideoTooltip -> internalCallScreenState.update { it.copy(displayVideoTooltip = false) } + is CallEvent.ShowGroupCallSafetyNumberChange -> { + viewModelScope.launch { + internalCallActions.emit(Action.ShowGroupCallSafetyNumberChangeDialog(event.identityRecords)) + } + } + CallEvent.ShowSwipeToSpeakerHint -> internalCallScreenState.update { it.copy(displaySwipeToSpeakerHint = true) } + CallEvent.ShowSwitchCameraTooltip -> internalCallScreenState.update { it.copy(displaySwitchCameraTooltip = true) } + CallEvent.ShowVideoTooltip -> internalCallScreenState.update { it.copy(displayVideoTooltip = true) } + CallEvent.ShowWifiToCellularPopup -> internalCallScreenState.update { it.copy(displayWifiToCellularPopup = true) } is CallEvent.StartCall -> startCall(event.isVideoCall) - CallEvent.SwitchToSpeaker -> Unit // TODO + CallEvent.SwitchToSpeaker -> { + viewModelScope.launch { + internalCallActions.emit(Action.SwitchToSpeaker) + } + } } } @@ -406,11 +420,22 @@ class CallViewModel( /** * Actions that require activity-level context (for example, to request permissions.) */ - enum class Action { + sealed interface Action { /** * Tries to enable local video via the normal toggle callback. Should display permissions * dialogs as necessary. */ - EnableVideo + data object EnableVideo : Action + + /** + * Display the safety number change dialog for the given untrusted identities. Since this dialog + * is not in compose-land, we delegate this as an action instead of embedding it in the screen state. + */ + data class ShowGroupCallSafetyNumberChangeDialog(val untrustedIdentities: List) : Action + + /** + * Immediately switch the user to speaker view + */ + data object SwitchToSpeaker : Action } } diff --git a/core-util-jvm/build.gradle.kts b/core-util-jvm/build.gradle.kts index c4cd947f82..36ab387f21 100644 --- a/core-util-jvm/build.gradle.kts +++ b/core-util-jvm/build.gradle.kts @@ -45,6 +45,10 @@ wire { } } +tasks.runKtlintCheckOverMainSourceSet { + dependsOn(":core-util-jvm:generateMainProtos") +} + dependencies { implementation(libs.kotlin.reflect) implementation(libs.kotlinx.coroutines.core)