diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java index bc75c45973..c4fed216a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java @@ -116,11 +116,11 @@ public class CallParticipantView extends ConstraintLayout { useLargeAvatar(); } - void setMirror(boolean mirror) { + public void setMirror(boolean mirror) { renderer.setMirror(mirror); } - void setScalingType(@NonNull RendererCommon.ScalingType scalingType) { + public void setScalingType(@NonNull RendererCommon.ScalingType scalingType) { renderer.setScalingType(scalingType); } @@ -128,7 +128,7 @@ public class CallParticipantView extends ConstraintLayout { renderer.setScalingType(scalingTypeMatchOrientation, scalingTypeMismatchOrientation); } - void setCallParticipant(@NonNull CallParticipant participant) { + public void setCallParticipant(@NonNull CallParticipant participant) { boolean participantChanged = recipientId == null || !recipientId.equals(participant.getRecipient().getId()); recipientId = participant.getRecipient().getId(); infoMode = participant.getRecipient().isBlocked() || isMissingMediaKeys(participant); @@ -221,7 +221,7 @@ public class CallParticipantView extends ConstraintLayout { return false; } - void setRenderInPip(boolean shouldRenderInPip) { + public void setRenderInPip(boolean shouldRenderInPip) { this.shouldRenderInPip = shouldRenderInPip; if (infoMode) { @@ -243,11 +243,15 @@ public class CallParticipantView extends ConstraintLayout { this.raiseHandAllowed = raiseHandAllowed; } + public void setCameraToggleOnClickListener(@Nullable View.OnClickListener onClickListener) { + switchCameraIconFrame.setOnClickListener(onClickListener); + } + /** * Adjust UI elements for the various self PIP positions. If called after a {@link TransitionManager#beginDelayedTransition(ViewGroup, Transition)}, * the changes to the UI elements will animate. */ - void setSelfPipMode(@NonNull SelfPipMode selfPipMode, boolean isMoreThanOneCameraAvailable) { + public void setSelfPipMode(@NonNull SelfPipMode selfPipMode, boolean isMoreThanOneCameraAvailable) { Preconditions.checkArgument(selfPipMode != SelfPipMode.NOT_SELF_PIP); if (this.selfPipMode == selfPipMode) { @@ -402,7 +406,7 @@ public class CallParticipantView extends ConstraintLayout { ViewUtil.setBottomMargin(audioIndicator, desiredMargin); } - void releaseRenderer() { + public void releaseRenderer() { renderer.release(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantRenderer.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantRenderer.kt index f3399d484c..cff7f5e86d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantRenderer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantRenderer.kt @@ -5,22 +5,19 @@ package org.thoughtcrime.securesms.components.webrtc.v2 -import androidx.compose.foundation.layout.BoxWithConstraints +import android.view.LayoutInflater +import android.widget.FrameLayout +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.unit.DpSize import androidx.compose.ui.viewinterop.AndroidView -import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarDrawable -import org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer -import org.thoughtcrime.securesms.compose.GlideImage -import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.webrtc.CallParticipantView import org.thoughtcrime.securesms.events.CallParticipant -import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.ringrtc.CameraState import org.webrtc.RendererCommon @@ -30,59 +27,38 @@ import org.webrtc.RendererCommon @Composable fun CallParticipantRenderer( callParticipant: CallParticipant, + renderInPip: Boolean, modifier: Modifier = Modifier, - force: Boolean = false + isLocalParticipant: Boolean = false, + isRaiseHandAllowed: Boolean = false, + selfPipMode: CallParticipantView.SelfPipMode = CallParticipantView.SelfPipMode.NOT_SELF_PIP, + onToggleCameraDirection: () -> Unit = {} ) { - BoxWithConstraints( - modifier = modifier - ) { - val maxWidth = constraints.maxWidth - val maxHeight = constraints.maxHeight - - val density = LocalDensity.current - val size = with(density) { - DpSize( - width = maxWidth.toDp(), - height = maxHeight.toDp() - ) - } - - val localRecipient = if (LocalInspectionMode.current) { - Recipient() - } else { - callParticipant.recipient - } - - val model = remember { - ProfileContactPhoto(localRecipient) - } - - val context = LocalContext.current - val fallback = remember { - FallbackAvatarDrawable(context, localRecipient.getFallbackAvatar()) - } - - GlideImage( - model = model, - imageSize = size, - fallback = fallback, - modifier = Modifier.fillMaxSize() - ) - - if (force || callParticipant.isVideoEnabled) { - AndroidView( - factory = ::TextureViewRenderer, - modifier = Modifier.fillMaxSize(), - onRelease = { it.release() } - ) { renderer -> - renderer.setMirror(callParticipant.cameraDirection == CameraState.Direction.FRONT) - renderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL) - - callParticipant.videoSink.lockableEglBase.performWithValidEglBase { - renderer.init(it) + if (LocalInspectionMode.current) { + Box(modifier.background(color = Color.Red)) + } else { + AndroidView( + factory = { LayoutInflater.from(it).inflate(R.layout.call_participant_item, FrameLayout(it), false) as CallParticipantView }, + modifier = modifier.fillMaxSize().background(color = Color.Red), + onRelease = { it.releaseRenderer() } + ) { view -> + view.setCallParticipant(callParticipant) + view.setMirror(isLocalParticipant && callParticipant.cameraState.activeDirection == CameraState.Direction.FRONT) + view.setScalingType( + if (callParticipant.isScreenSharing && !isLocalParticipant) { + RendererCommon.ScalingType.SCALE_ASPECT_FIT + } else { + RendererCommon.ScalingType.SCALE_ASPECT_FILL } + ) + view.setRenderInPip(renderInPip) + view.setRaiseHandAllowed(isRaiseHandAllowed) - renderer.attachBroadcastVideoSink(callParticipant.videoSink) + if (selfPipMode != CallParticipantView.SelfPipMode.NOT_SELF_PIP) { + view.setSelfPipMode(selfPipMode, callParticipant.cameraState.cameraCount > 1) + view.setCameraToggleOnClickListener { onToggleCameraDirection() } + } else { + view.setCameraToggleOnClickListener(null) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantsPager.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantsPager.kt index acdbd4a8c0..7ac0d46e4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantsPager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantsPager.kt @@ -46,6 +46,7 @@ fun CallParticipantsPager( 1 -> { CallParticipantRenderer( callParticipant = callParticipantsPagerState.focusedParticipant, + renderInPip = callParticipantsPagerState.isRenderInPip, modifier = Modifier.fillMaxSize() ) } 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 f8f7e19212..eaf76c94ed 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 @@ -56,6 +56,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode @@ -73,12 +74,14 @@ import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.TriggerAlignedPopupState import org.signal.core.util.DimensionUnit import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.webrtc.CallParticipantView import org.thoughtcrime.securesms.components.webrtc.WebRtcLocalRenderState import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette import org.thoughtcrime.securesms.events.CallParticipant import org.thoughtcrime.securesms.events.GroupCallReactionEvent import org.thoughtcrime.securesms.events.WebRtcViewModel import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.ringrtc.CameraState import kotlin.math.max import kotlin.math.round import kotlin.time.Duration.Companion.seconds @@ -170,7 +173,6 @@ fun CallScreen( val maxSheetHeight = round(constraints.maxHeight * 0.66f) val maxOffset = maxHeight - maxSheetHeight - var offset by remember { mutableFloatStateOf(0f) } var peekHeight by remember { mutableFloatStateOf(88f) } BottomSheetScaffold( @@ -192,7 +194,7 @@ fun CallScreen( .padding(top = SHEET_TOP_PADDING.dp, bottom = SHEET_BOTTOM_PADDING.dp) .height(DimensionUnit.PIXELS.toDp(maxSheetHeight).dp) .onGloballyPositioned { - offset = scaffoldState.bottomSheetState.requireOffset() + val offset = it.positionInRoot().y val current = maxHeight - offset - DimensionUnit.DP.toPixels(peekHeight) val maximum = maxHeight - maxOffset - DimensionUnit.DP.toPixels(peekHeight) @@ -242,6 +244,7 @@ fun CallScreen( onPipClick = onLocalPictureInPictureClicked, onControlsToggled = onControlsToggled, callScreenController = callScreenController, + onToggleCameraDirection = callScreenControlsListener::onCameraDirectionChanged, modifier = if (isPortrait) { Modifier.padding(bottom = padding) } else Modifier @@ -282,7 +285,9 @@ fun CallScreen( callStatus = callScreenState.callStatus, onNavigationClick = onNavigationClick, onCallInfoClick = onCallInfoClick, + onCameraToggleClick = callScreenControlsListener::onCameraDirectionChanged, isLocalVideoEnabled = localParticipant.isVideoEnabled, + isMoreThanOneCameraAvailable = localParticipant.isMoreThanOneCameraAvailable, modifier = Modifier.padding(bottom = padding) ) } @@ -331,6 +336,7 @@ private fun BoxScope.Viewport( callScreenController: CallScreenController, onPipClick: () -> Unit, onControlsToggled: (Boolean) -> Unit, + onToggleCameraDirection: () -> Unit, modifier: Modifier = Modifier ) { if (webRtcCallState.isPreJoinOrNetworkUnavailable) { @@ -448,6 +454,7 @@ private fun BoxScope.Viewport( localParticipant = localParticipant, localRenderState = localRenderState, onClick = onPipClick, + onToggleCameraDirection = onToggleCameraDirection, modifier = modifier ) } @@ -463,6 +470,8 @@ private fun LargeLocalVideoRenderer( ) { CallParticipantRenderer( callParticipant = localParticipant, + isLocalParticipant = true, + renderInPip = false, modifier = modifier .fillMaxSize() ) @@ -499,6 +508,9 @@ private fun TinyLocalVideoRenderer( CallParticipantRenderer( callParticipant = localParticipant, + isLocalParticipant = true, + renderInPip = true, + selfPipMode = CallParticipantView.SelfPipMode.MINI_SELF_PIP, modifier = modifier .padding(padding) .height(height) @@ -516,6 +528,7 @@ private fun SmallMoveableLocalVideoRenderer( localParticipant: CallParticipant, localRenderState: WebRtcLocalRenderState, onClick: () -> Unit, + onToggleCameraDirection: () -> Unit, modifier: Modifier = Modifier ) { val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT @@ -542,6 +555,14 @@ private fun SmallMoveableLocalVideoRenderer( ) { CallParticipantRenderer( callParticipant = localParticipant, + isLocalParticipant = true, + renderInPip = true, + selfPipMode = if (localRenderState == WebRtcLocalRenderState.EXPANDED) { + CallParticipantView.SelfPipMode.EXPANDED_SELF_PIP + } else { + CallParticipantView.SelfPipMode.NORMAL_SELF_PIP + }, + onToggleCameraDirection = onToggleCameraDirection, modifier = Modifier .fillMaxSize() .clip(MaterialTheme.shapes.medium) @@ -631,7 +652,7 @@ private fun CallScreenPreview() { Previews.Preview { CallScreen( callRecipient = Recipient(systemContactName = "Test User"), - webRtcCallState = WebRtcViewModel.State.CALL_CONNECTED, + webRtcCallState = WebRtcViewModel.State.CALL_PRE_JOIN, isRemoteVideoOffer = false, isInPipMode = false, callScreenState = CallScreenState( @@ -653,7 +674,11 @@ private fun CallScreenPreview() { isResolving = false, isSelf = true ), - isVideoEnabled = true + isVideoEnabled = true, + cameraState = CameraState( + CameraState.Direction.FRONT, + 2 + ) ), localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE, callScreenDialogType = CallScreenDialogType.NONE, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenPreJoinOverlay.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenPreJoinOverlay.kt new file mode 100644 index 0000000000..206bfd5dab --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenPreJoinOverlay.kt @@ -0,0 +1,248 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc.v2 + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import org.signal.core.ui.compose.NightPreview +import org.signal.core.ui.compose.Previews +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.avatar.AvatarImage +import org.thoughtcrime.securesms.recipients.Recipient + +/** + * Pre-join call screen overlay. + * + * This overlay is displayed when the user has entered the calling screen but has not yet joined a call, and is + * positioned above the controls sheet. + */ +@Composable +fun CallScreenPreJoinOverlay( + callRecipient: Recipient, + callStatus: String?, + isMoreThanOneCameraAvailable: Boolean, + isLocalVideoEnabled: Boolean, + modifier: Modifier = Modifier, + onNavigationClick: () -> Unit = {}, + onCallInfoClick: () -> Unit = {}, + onCameraToggleClick: () -> Unit = {} +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxSize() + .background(color = Color(0f, 0f, 0f, 0.4f)) + ) { + CallScreenTopAppBar( + onNavigationClick = onNavigationClick, + onCallInfoClick = onCallInfoClick + ) + + AvatarImage( + recipient = callRecipient, + modifier = Modifier + .padding(top = 8.dp) + .size(96.dp) + ) + + Text( + text = callRecipient.getDisplayName(LocalContext.current), + style = MaterialTheme.typography.headlineMedium, + color = Color.White, + modifier = Modifier.padding(top = 16.dp) + ) + + if (callStatus != null) { + Text( + text = callStatus, + style = MaterialTheme.typography.bodyMedium, + color = Color.White, + modifier = Modifier.padding(top = 8.dp) + ) + } + + if (!isLocalVideoEnabled) { + Spacer(modifier = Modifier.weight(1f)) + + Icon( + painter = painterResource( + id = R.drawable.symbol_video_slash_24 + ), + contentDescription = null, + tint = Color.White, + modifier = Modifier.padding(bottom = 8.dp) + ) + + Text( + text = stringResource(id = R.string.CallScreenPreJoinOverlay__your_camera_is_off), + color = Color.White + ) + + Spacer(modifier = Modifier.weight(1f)) + } + + if (isLocalVideoEnabled && isMoreThanOneCameraAvailable) { + Spacer(modifier = Modifier.weight(1f)) + + Box(modifier = Modifier.fillMaxWidth()) { + CallCameraDirectionToggle( + onClick = onCameraToggleClick, + modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp) + ) + } + } + } +} + +@Composable +private fun CallCameraDirectionToggle( + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + IconButton( + onClick = onClick, + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = Color.White + ), + shape = CircleShape, + modifier = modifier.size(48.dp) + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.symbol_switch_24), + contentDescription = stringResource(R.string.CallScreen__change_camera_direction) + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CallScreenTopAppBar( + callRecipient: Recipient? = null, + callStatus: String? = null, + onNavigationClick: () -> Unit = {}, + onCallInfoClick: () -> Unit = {}, + modifier: Modifier = Modifier +) { + val textShadow = remember { + Shadow( + color = Color(0f, 0f, 0f, 0.25f), + blurRadius = 4f + ) + } + + TopAppBar( + modifier = modifier, + colors = TopAppBarDefaults.topAppBarColors().copy( + containerColor = Color.Transparent + ), + title = { + Column { + if (callRecipient != null) { + Text( + text = callRecipient.getDisplayName(LocalContext.current), + style = MaterialTheme.typography.titleMedium.copy(shadow = textShadow) + ) + } + + if (callStatus != null) { + Text( + text = callStatus, + style = MaterialTheme.typography.bodyMedium.copy(shadow = textShadow), + modifier = Modifier.padding(top = 2.dp) + ) + } + } + }, + navigationIcon = { + IconButton( + onClick = onNavigationClick + ) { + Icon( + painter = painterResource(id = R.drawable.symbol_arrow_start_24), + contentDescription = stringResource(id = R.string.CallScreenTopBar__go_back), + tint = Color.White + ) + } + }, + actions = { + IconButton( + onClick = onCallInfoClick, + modifier = Modifier.padding(16.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.symbol_info_24), + contentDescription = stringResource(id = R.string.CallScreenTopBar__call_information), + tint = Color.White + ) + } + } + ) +} + +@NightPreview +@Composable +fun CallScreenTopBarPreview() { + Previews.Preview { + CallScreenTopBar( + callRecipient = Recipient(systemContactName = "Test User"), + callStatus = null + ) + } +} + +@NightPreview +@Composable +fun CallScreenPreJoinOverlayPreview() { + Previews.Preview { + CallScreenPreJoinOverlay( + callRecipient = Recipient(systemContactName = "Test User"), + callStatus = stringResource(R.string.Recipient_unknown), + isLocalVideoEnabled = false, + isMoreThanOneCameraAvailable = false + ) + } +} + +@NightPreview +@Composable +fun CallScreenPreJoinOverlayWithTogglePreview() { + Previews.Preview { + CallScreenPreJoinOverlay( + callRecipient = Recipient(systemContactName = "Test User"), + callStatus = stringResource(R.string.Recipient_unknown), + isLocalVideoEnabled = true, + isMoreThanOneCameraAvailable = true + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenTopBar.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenTopBar.kt index 85cad50bb5..8581cb03da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenTopBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenTopBar.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Signal Messenger, LLC + * Copyright 2025 Signal Messenger, LLC * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,34 +7,12 @@ package org.thoughtcrime.securesms.components.webrtc.v2 import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shadow -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import org.signal.core.ui.compose.NightPreview -import org.signal.core.ui.compose.Previews -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.avatar.AvatarImage import org.thoughtcrime.securesms.recipients.Recipient /** @@ -66,156 +44,3 @@ fun CallScreenTopBar( ) } } - -@Composable -fun CallScreenPreJoinOverlay( - callRecipient: Recipient, - callStatus: String?, - isLocalVideoEnabled: Boolean, - modifier: Modifier = Modifier, - onNavigationClick: () -> Unit = {}, - onCallInfoClick: () -> Unit = {} -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxSize() - .background(color = Color(0f, 0f, 0f, 0.4f)) - ) { - CallScreenTopAppBar( - onNavigationClick = onNavigationClick, - onCallInfoClick = onCallInfoClick - ) - - AvatarImage( - recipient = callRecipient, - modifier = Modifier - .padding(top = 8.dp) - .size(96.dp) - ) - - Text( - text = callRecipient.getDisplayName(LocalContext.current), - style = MaterialTheme.typography.headlineMedium, - color = Color.White, - modifier = Modifier.padding(top = 16.dp) - ) - - if (callStatus != null) { - Text( - text = callStatus, - style = MaterialTheme.typography.bodyMedium, - color = Color.White, - modifier = Modifier.padding(top = 8.dp) - ) - } - - if (!isLocalVideoEnabled) { - Spacer(modifier = Modifier.weight(1f)) - - Icon( - painter = painterResource( - id = R.drawable.symbol_video_slash_24 - ), - contentDescription = null, - tint = Color.White, - modifier = Modifier.padding(bottom = 8.dp) - ) - - Text( - text = stringResource(id = R.string.CallScreenPreJoinOverlay__your_camera_is_off), - color = Color.White - ) - - Spacer(modifier = Modifier.weight(1f)) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun CallScreenTopAppBar( - callRecipient: Recipient? = null, - callStatus: String? = null, - onNavigationClick: () -> Unit = {}, - onCallInfoClick: () -> Unit = {}, - modifier: Modifier = Modifier -) { - val textShadow = remember { - Shadow( - color = Color(0f, 0f, 0f, 0.25f), - blurRadius = 4f - ) - } - - TopAppBar( - modifier = modifier, - colors = TopAppBarDefaults.topAppBarColors().copy( - containerColor = Color.Transparent - ), - title = { - Column { - if (callRecipient != null) { - Text( - text = callRecipient.getDisplayName(LocalContext.current), - style = MaterialTheme.typography.titleMedium.copy(shadow = textShadow) - ) - } - - if (callStatus != null) { - Text( - text = callStatus, - style = MaterialTheme.typography.bodyMedium.copy(shadow = textShadow), - modifier = Modifier.padding(top = 2.dp) - ) - } - } - }, - navigationIcon = { - IconButton( - onClick = onNavigationClick - ) { - Icon( - painter = painterResource(id = R.drawable.symbol_arrow_start_24), - contentDescription = stringResource(id = R.string.CallScreenTopBar__go_back), - tint = Color.White - ) - } - }, - actions = { - IconButton( - onClick = onCallInfoClick, - modifier = Modifier.padding(16.dp) - ) { - Icon( - painter = painterResource(id = R.drawable.symbol_info_24), - contentDescription = stringResource(id = R.string.CallScreenTopBar__call_information), - tint = Color.White - ) - } - } - ) -} - -@NightPreview -@Composable -fun CallScreenTopBarPreview() { - Previews.Preview { - CallScreenTopBar( - callRecipient = Recipient(systemContactName = "Test User"), - callStatus = null - ) - } -} - -@NightPreview -@Composable -fun CallScreenPreJoinOverlayPreview() { - Previews.Preview { - CallScreenPreJoinOverlay( - callRecipient = Recipient(systemContactName = "Test User"), - callStatus = stringResource(R.string.Recipient_unknown), - isLocalVideoEnabled = false - ) - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0212bf2cd2..cfeaea6b05 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3373,6 +3373,9 @@ Answer without video + + Change camera direction + Audio output