From 94f997a97ddf6e7fa71c4667f870c015925acc80 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 27 Oct 2023 12:58:09 +0200 Subject: [PATCH] Wire layout chooser with the UI and call action to choose layout, update the layout on choice --- .../video/android/ui/call/CallScreen.kt | 26 ++ .../video/android/ui/call/LayoutChooser.kt | 321 ++++++++++++++++++ .../api/stream-video-android-compose.api | 4 +- .../components/call/activecall/CallContent.kt | 34 +- .../call/renderer/ParticipantsLayout.kt | 3 +- .../api/stream-video-android-core.api | 7 + .../android/core/call/state/CallAction.kt | 5 + 7 files changed, 397 insertions(+), 3 deletions(-) create mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt index 7e36679168..1ea9e46711 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt @@ -54,12 +54,15 @@ import io.getstream.video.android.compose.ui.components.call.activecall.CallCont import io.getstream.video.android.compose.ui.components.call.controls.ControlActions import io.getstream.video.android.compose.ui.components.call.controls.actions.CancelCallAction import io.getstream.video.android.compose.ui.components.call.controls.actions.ChatDialogAction +import io.getstream.video.android.compose.ui.components.call.controls.actions.DefaultOnCallActionHandler import io.getstream.video.android.compose.ui.components.call.controls.actions.FlipCameraAction import io.getstream.video.android.compose.ui.components.call.controls.actions.SettingsAction import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleCameraAction import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleMicrophoneAction +import io.getstream.video.android.compose.ui.components.call.renderer.LayoutType import io.getstream.video.android.core.Call import io.getstream.video.android.core.RealtimeConnection +import io.getstream.video.android.core.call.state.ChooseLayout import io.getstream.video.android.mock.StreamMockUtils import io.getstream.video.android.mock.mockCall import kotlinx.coroutines.delay @@ -77,8 +80,10 @@ fun CallScreen( val isMicrophoneEnabled by call.microphone.isEnabled.collectAsState() val speakingWhileMuted by call.state.speakingWhileMuted.collectAsState() var isShowingSettingMenu by remember { mutableStateOf(false) } + var isShowingLayoutChooseMenu by remember { mutableStateOf(false) } var isShowingReactionsMenu by remember { mutableStateOf(false) } var isShowingAvailableDeviceMenu by remember { mutableStateOf(false) } + var layout by remember { mutableStateOf(LayoutType.DYNAMIC) } var unreadCount by remember { mutableIntStateOf(0) } val chatState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden, @@ -113,8 +118,18 @@ fun CallScreen( CallContent( modifier = Modifier.background(color = VideoTheme.colors.appBackground), call = call, + layout = layout, enableInPictureInPicture = true, enableDiagnostics = BuildConfig.DEBUG, + onCallAction = { + when (it) { + ChooseLayout -> isShowingLayoutChooseMenu = true + else -> DefaultOnCallActionHandler.onCallAction( + call, + it, + ) + } + }, onBackPressed = { if (chatState.currentValue == ModalBottomSheetValue.Expanded) { scope.launch { chatState.hide() } @@ -263,6 +278,17 @@ fun CallScreen( ) } + if (isShowingLayoutChooseMenu) { + LayoutChooser( + onLayoutChoice = { + layout = it + isShowingLayoutChooseMenu = false + }, + current = layout, + onDismiss = { isShowingLayoutChooseMenu = false }, + ) + } + if (isShowingAvailableDeviceMenu) { AvailableDeviceMenu( call = call, diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt new file mode 100644 index 0000000000..cb2592230b --- /dev/null +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalLayoutApi::class) + +package io.getstream.video.android.ui.call + +import android.content.res.Configuration +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +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.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AutoAwesome +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.ui.components.call.renderer.LayoutType +import io.getstream.video.android.mock.StreamMockUtils + +private data class LayoutChooserDataItem( + val which: LayoutType, + val text: String = "", +) + +private val layouts = arrayOf( + LayoutChooserDataItem(LayoutType.DYNAMIC, "Dynamic"), + LayoutChooserDataItem(LayoutType.SPOTLIGHT, "Spotlight"), + LayoutChooserDataItem(LayoutType.GRID, "Grid"), +) + +/** + * Reactions menu. The reaction menu is a dialog displaying the list of reactions found in + * [DefaultReactionsMenuData]. + * @param current + * @param onDismiss on dismiss listener. + */ +@Composable +internal fun LayoutChooser( + current: LayoutType, + onLayoutChoice: (LayoutType) -> Unit, + onDismiss: () -> Unit, +) { + Dialog(onDismiss) { + Row(Modifier.background(VideoTheme.colors.appBackground)) { + layouts.forEach { + LayoutItem( + current = current, + item = it, + onClicked = onLayoutChoice, + ) + } + } + } +} + +@Composable +private fun LayoutItem( + modifier: Modifier = Modifier, + current: LayoutType, + item: LayoutChooserDataItem, + onClicked: (LayoutType) -> Unit = {}, +) { + val border = + if (current == item.which) BorderStroke(2.dp, VideoTheme.colors.primaryAccent) else null + Card( + modifier = modifier + .clickable { onClicked(item.which) } + .padding(12.dp), + backgroundColor = VideoTheme.colors.appBackground, + elevation = 3.dp, + border = border, + ) { + Column { + Box( + modifier = Modifier + .size(84.dp, 84.dp) + .padding(2.dp), + ) { + when (item.which) { + LayoutType.DYNAMIC -> { + DynamicRepresentation() + } + + LayoutType.SPOTLIGHT -> { + SpotlightRepresentation() + } + + LayoutType.GRID -> { + GridRepresentation() + } + } + } + Text( + modifier = Modifier + .align(CenterHorizontally) + .padding(12.dp), + textAlign = TextAlign.Center, + text = item.text, + color = VideoTheme.colors.textHighEmphasis, + ) + } + } +} + +@Composable +private fun DynamicRepresentation() { + Column { + Card( + modifier = Modifier + .weight(2f) + .fillMaxWidth() + .padding(2.dp), + backgroundColor = VideoTheme.colors.participantContainerBackground, + ) { + } + + Row(modifier = Modifier.weight(1f)) { + Card( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .padding(2.dp), + backgroundColor = VideoTheme.colors.participantContainerBackground, + ) { + } + Card( + backgroundColor = VideoTheme.colors.appBackground, + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .padding(2.dp), + ) { + Icon( + modifier = Modifier + .fillMaxSize() + .padding(2.dp), + tint = VideoTheme.colors.participantContainerBackground, + imageVector = Icons.Rounded.AutoAwesome, + contentDescription = "dynamic", + ) + } + } + } +} + +@Composable +private fun GridRepresentation() { + Column { + repeat(3) { + Row { + repeat(3) { + Card( + modifier = Modifier + .aspectRatio(1f) + .weight(1f) + .padding(2.dp), + backgroundColor = VideoTheme.colors.participantContainerBackground, + ) { + } + } + } + } + } +} + +@Composable +private fun SpotlightRepresentation() { + Column { + Card( + modifier = Modifier + .weight(2f) + .fillMaxWidth() + .padding(2.dp), + backgroundColor = VideoTheme.colors.participantContainerBackground, + ) { + } + + Row(modifier = Modifier.weight(1f)) { + repeat(3) { + Card( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .padding(2.dp), + backgroundColor = VideoTheme.colors.participantContainerBackground, + ) { + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun LayoutChooserPreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutChooser( + current = LayoutType.GRID, + onLayoutChoice = {}, + onDismiss = {}, + ) + } +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun LayoutChooserPreviewDark() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutChooser( + current = LayoutType.GRID, + onLayoutChoice = {}, + onDismiss = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun GridItemPreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutItem( + current = LayoutType.GRID, + item = layouts[2], + ) + } +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun GridItemPreviewDark() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutItem( + current = LayoutType.GRID, + item = layouts[2], + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun SpotlightItemPreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutItem( + current = LayoutType.GRID, + item = layouts[1], + ) + } +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun SpotlightItemPreviewDark() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutItem( + current = LayoutType.GRID, + item = layouts[1], + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun DynamicItemPreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutItem( + current = LayoutType.GRID, + item = layouts[0], + ) + } +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun DynamicItemPreviewDark() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutItem( + current = LayoutType.GRID, + item = layouts[0], + ) + } +} diff --git a/stream-video-android-compose/api/stream-video-android-compose.api b/stream-video-android-compose/api/stream-video-android-compose.api index 64a37b4e61..2bbc14ea14 100644 --- a/stream-video-android-compose/api/stream-video-android-compose.api +++ b/stream-video-android-compose/api/stream-video-android-compose.api @@ -625,7 +625,7 @@ public final class io/getstream/video/android/compose/ui/components/call/Composa } public final class io/getstream/video/android/compose/ui/components/call/activecall/CallContentKt { - public static final fun CallContent (Lio/getstream/video/android/core/Call;Landroidx/compose/ui/Modifier;ZLio/getstream/video/android/compose/permission/VideoPermissionsState;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lio/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;ZLkotlin/jvm/functions/Function3;ZLandroidx/compose/runtime/Composer;III)V + public static final fun CallContent (Lio/getstream/video/android/core/Call;Landroidx/compose/ui/Modifier;Lio/getstream/video/android/compose/ui/components/call/renderer/LayoutType;ZLio/getstream/video/android/compose/permission/VideoPermissionsState;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lio/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;ZLkotlin/jvm/functions/Function3;ZLandroidx/compose/runtime/Composer;III)V } public final class io/getstream/video/android/compose/ui/components/call/activecall/ComposableSingletons$CallContentKt { @@ -635,12 +635,14 @@ public final class io/getstream/video/android/compose/ui/components/call/activec public static field lambda-3 Lkotlin/jvm/functions/Function3; public static field lambda-4 Lkotlin/jvm/functions/Function2; public static field lambda-5 Lkotlin/jvm/functions/Function2; + public static field lambda-6 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_video_android_compose_release ()Lkotlin/jvm/functions/Function6; public final fun getLambda-2$stream_video_android_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-3$stream_video_android_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-4$stream_video_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-5$stream_video_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-6$stream_video_android_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/video/android/compose/ui/components/call/activecall/internal/ComposableSingletons$InviteUsersDialogKt { diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt index 7935dafb01..e27bc2d099 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt @@ -29,7 +29,11 @@ import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.Scaffold +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AutoAwesomeMosaic import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -43,6 +47,7 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.coerceAtLeast import androidx.compose.ui.unit.dp @@ -59,6 +64,7 @@ import io.getstream.video.android.compose.ui.components.call.CallAppBar import io.getstream.video.android.compose.ui.components.call.controls.ControlActions import io.getstream.video.android.compose.ui.components.call.controls.actions.DefaultOnCallActionHandler import io.getstream.video.android.compose.ui.components.call.diagnostics.CallDiagnosticsContent +import io.getstream.video.android.compose.ui.components.call.renderer.LayoutType import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantsLayout import io.getstream.video.android.compose.ui.components.call.renderer.RegularVideoRendererStyle @@ -67,8 +73,10 @@ import io.getstream.video.android.compose.ui.components.video.VideoRenderer import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState import io.getstream.video.android.core.call.state.CallAction +import io.getstream.video.android.core.call.state.ChooseLayout import io.getstream.video.android.mock.StreamMockUtils import io.getstream.video.android.mock.mockCall +import io.getstream.video.android.ui.common.R /** * Represents the UI in an Active call that shows participants and their video, as well as some @@ -76,6 +84,7 @@ import io.getstream.video.android.mock.mockCall * * @param call The call includes states and will be rendered with participants. * @param modifier Modifier for styling. + * @param layout the type of layout that the call content will display [LayoutType] * @param onBackPressed Handler when the user taps on the back button. * @param permissions Android permissions that should be required to render a video call properly. * @param onCallAction Handler when the user triggers a Call Control Action. @@ -92,6 +101,7 @@ import io.getstream.video.android.mock.mockCall public fun CallContent( call: Call, modifier: Modifier = Modifier, + layout: LayoutType = LayoutType.DYNAMIC, isShowingOverlayAppBar: Boolean = true, permissions: VideoPermissionsState = rememberCallPermissionsState(call = call), onBackPressed: () -> Unit = {}, @@ -99,7 +109,9 @@ public fun CallContent( appBarContent: @Composable (call: Call) -> Unit = { CallAppBar( call = call, - leadingContent = null, + leadingContent = { + LayoutChoiceLeadingContent(onCallAction) + }, onCallAction = onCallAction, ) }, @@ -119,6 +131,7 @@ public fun CallContent( }, videoContent: @Composable RowScope.(call: Call) -> Unit = { ParticipantsLayout( + layoutType = layout, call = call, modifier = Modifier .fillMaxSize() @@ -226,6 +239,25 @@ public fun CallContent( } } +@Composable +internal fun LayoutChoiceLeadingContent(onCallAction: (CallAction) -> Unit) { + IconButton( + onClick = { onCallAction.invoke(ChooseLayout) }, + modifier = Modifier.padding( + start = VideoTheme.dimens.callAppBarLeadingContentSpacingStart, + end = VideoTheme.dimens.callAppBarLeadingContentSpacingEnd, + ), + ) { + Icon( + imageVector = Icons.Rounded.AutoAwesomeMosaic, + contentDescription = stringResource( + id = R.string.stream_video_back_button_content_description, + ), + tint = VideoTheme.colors.callDescription, + ) + } +} + /** * Renders the default PiP content, using the call state that's provided. * diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsLayout.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsLayout.kt index e1c6dc459b..1484b157be 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsLayout.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsLayout.kt @@ -16,6 +16,7 @@ package io.getstream.video.android.compose.ui.components.call.renderer +import android.util.Log import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf @@ -76,7 +77,7 @@ public fun ParticipantsLayout( val screenSharingSession = call.state.screenSharingSession.collectAsStateWithLifecycle() val screenSharing = screenSharingSession.value val pinnedParticipants by call.state.pinnedParticipants.collectAsStateWithLifecycle() - val showSpotlight by remember(pinnedParticipants) { + val showSpotlight by remember(key1 = pinnedParticipants, key2 = layoutType) { derivedStateOf { when (layoutType) { LayoutType.GRID -> false diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index 0794454784..3eb1a5d911 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -1010,6 +1010,13 @@ public final class io/getstream/video/android/core/call/state/ChatDialog : io/ge public fun toString ()Ljava/lang/String; } +public final class io/getstream/video/android/core/call/state/ChooseLayout : io/getstream/video/android/core/call/state/CallAction { + public static final field INSTANCE Lio/getstream/video/android/core/call/state/ChooseLayout; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public class io/getstream/video/android/core/call/state/CustomAction : io/getstream/video/android/core/call/state/CallAction { public fun (Ljava/util/Map;Ljava/lang/String;)V public synthetic fun (Ljava/util/Map;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/state/CallAction.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/state/CallAction.kt index e57953a2ce..6fc7bd3b58 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/state/CallAction.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/state/CallAction.kt @@ -92,6 +92,11 @@ public data object Settings : CallAction */ public data object Reaction : CallAction +/** + * Action to show a layout chooser. + */ +public data object ChooseLayout : CallAction + /** * Action to invite other users to a call. */