mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-26 05:58:09 +00:00
Calling UI camera toggle in PIP and in pre-join state.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ fun CallParticipantsPager(
|
||||
1 -> {
|
||||
CallParticipantRenderer(
|
||||
callParticipant = callParticipantsPagerState.focusedParticipant,
|
||||
renderInPip = callParticipantsPagerState.isRenderInPip,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3373,6 +3373,9 @@
|
||||
<!-- Label with hyphenation. Translation can use soft hyphen - Unicode U+00AD -->
|
||||
<string name="WebRtcCallScreen__answer_without_video">Answer without video</string>
|
||||
|
||||
<!-- CallScreen -->
|
||||
<string name="CallScreen__change_camera_direction">Change camera direction</string>
|
||||
|
||||
<!-- WebRtcAudioOutputToggle -->
|
||||
<!-- Label for a dialog asking the user to switch the audio output device during a call -->
|
||||
<string name="WebRtcAudioOutputToggle__audio_output">Audio output</string>
|
||||
|
||||
Reference in New Issue
Block a user