Add turn on your video tooltip to call screen v2.

This commit is contained in:
Alex Hart 2024-09-24 14:04:47 -03:00 committed by Greyson Parrelli
parent 5552455c2e
commit eaf81e56d6
7 changed files with 153 additions and 15 deletions

View file

@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
import org.thoughtcrime.securesms.util.FullscreenHelper import org.thoughtcrime.securesms.util.FullscreenHelper
import org.thoughtcrime.securesms.util.VibrateUtil import org.thoughtcrime.securesms.util.VibrateUtil
import org.thoughtcrime.securesms.util.viewModel import org.thoughtcrime.securesms.util.viewModel
@ -101,6 +102,8 @@ class CallActivity : BaseActivity(), CallControlsCallback {
viewModel.callActions.collect { viewModel.callActions.collect {
when (it) { when (it) {
CallViewModel.Action.EnableVideo -> onVideoToggleClick(true) 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() viewModel.hangup()
} }
override fun onVideoTooltipDismissed() {
viewModel.onVideoTooltipDismissed()
}
private fun observeCallEvents() { private fun observeCallEvents() {
webRtcCallViewModel.events.observe(this) { event -> webRtcCallViewModel.events.observe(this) { event ->
viewModel.onCallEvent(event) viewModel.onCallEvent(event)

View file

@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.util.RemoteConfig
*/ */
@Composable @Composable
fun CallControls( fun CallControls(
displayVideoTooltip: Boolean,
callControlsState: CallControlsState, callControlsState: CallControlsState,
callControlsCallback: CallControlsCallback, callControlsCallback: CallControlsCallback,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@ -96,10 +97,16 @@ fun CallControls(
val hasCameraPermission = ContextCompat.checkSelfPermission(LocalContext.current, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED val hasCameraPermission = ContextCompat.checkSelfPermission(LocalContext.current, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
if (callControlsState.displayVideoToggle) { if (callControlsState.displayVideoToggle) {
ToggleVideoButton( CallScreenTooltipBox(
isVideoEnabled = callControlsState.isVideoEnabled && hasCameraPermission, text = stringResource(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video),
onChange = callControlsCallback::onVideoToggleClick 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 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, startCallButtonText = R.string.WebRtcCallView__start_call,
displayEndCallButton = true displayEndCallButton = true
), ),
displayVideoTooltip = false,
callControlsCallback = CallControlsCallback.Empty callControlsCallback = CallControlsCallback.Empty
) )
} }
@ -179,6 +187,7 @@ interface CallControlsCallback {
fun onAdditionalActionsClick() fun onAdditionalActionsClick()
fun onStartCallClick(isVideoCall: Boolean) fun onStartCallClick(isVideoCall: Boolean)
fun onEndCallClick() fun onEndCallClick()
fun onVideoTooltipDismissed()
object Empty : CallControlsCallback { object Empty : CallControlsCallback {
override fun onAudioDeviceSheetDisplayChanged(displayed: Boolean) = Unit override fun onAudioDeviceSheetDisplayChanged(displayed: Boolean) = Unit
@ -189,6 +198,7 @@ interface CallControlsCallback {
override fun onAdditionalActionsClick() = Unit override fun onAdditionalActionsClick() = Unit
override fun onStartCallClick(isVideoCall: Boolean) = Unit override fun onStartCallClick(isVideoCall: Boolean) = Unit
override fun onEndCallClick() = Unit override fun onEndCallClick() = Unit
override fun onVideoTooltipDismissed() = Unit
} }
} }

View file

@ -154,6 +154,7 @@ fun CallScreen(
CallControls( CallControls(
callControlsState = callControlsState, callControlsState = callControlsState,
callControlsCallback = callControlsCallback, callControlsCallback = callControlsCallback,
displayVideoTooltip = callScreenState.displayVideoTooltip,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.alpha(callControlsAlpha) .alpha(callControlsAlpha)

View file

@ -25,7 +25,11 @@ data class CallScreenState(
val hangup: Hangup? = null, val hangup: Hangup? = null,
val callControlsChange: CallControlsChange? = null, val callControlsChange: CallControlsChange? = null,
val callStatus: CallString? = 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( data class Hangup(
val hangupMessageType: HangupMessage.Type, val hangupMessageType: HangupMessage.Type,

View file

@ -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")
}
}
}
}

View file

@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel 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.dependencies.AppDependencies
import org.thoughtcrime.securesms.events.WebRtcViewModel import org.thoughtcrime.securesms.events.WebRtcViewModel
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
@ -144,17 +145,30 @@ class CallViewModel(
internalDialog.update { CallScreenDialogType.NONE } internalDialog.update { CallScreenDialogType.NONE }
} }
fun onVideoTooltipDismissed() {
webRtcCallViewModel.onDismissedVideoTooltip()
internalCallScreenState.update { it.copy(displayVideoTooltip = false) }
}
fun onCallEvent(event: CallEvent) { fun onCallEvent(event: CallEvent) {
when (event) { when (event) {
CallEvent.DismissSwitchCameraTooltip -> Unit // TODO CallEvent.DismissSwitchCameraTooltip -> internalCallScreenState.update { it.copy(displaySwitchCameraTooltip = false) }
CallEvent.DismissVideoTooltip -> Unit // TODO CallEvent.DismissVideoTooltip -> internalCallScreenState.update { it.copy(displayVideoTooltip = false) }
is CallEvent.ShowGroupCallSafetyNumberChange -> Unit // TODO is CallEvent.ShowGroupCallSafetyNumberChange -> {
CallEvent.ShowSwipeToSpeakerHint -> Unit // TODO viewModelScope.launch {
CallEvent.ShowSwitchCameraTooltip -> Unit // TODO internalCallActions.emit(Action.ShowGroupCallSafetyNumberChangeDialog(event.identityRecords))
CallEvent.ShowVideoTooltip -> Unit // TODO }
CallEvent.ShowWifiToCellularPopup -> Unit // TODO }
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) 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.) * 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 * Tries to enable local video via the normal toggle callback. Should display permissions
* dialogs as necessary. * 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<IdentityRecord>) : Action
/**
* Immediately switch the user to speaker view
*/
data object SwitchToSpeaker : Action
} }
} }

View file

@ -45,6 +45,10 @@ wire {
} }
} }
tasks.runKtlintCheckOverMainSourceSet {
dependsOn(":core-util-jvm:generateMainProtos")
}
dependencies { dependencies {
implementation(libs.kotlin.reflect) implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)