From a6a6194ea05ee1fd249233c91d2cd332f3b293f9 Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Wed, 25 Oct 2023 14:56:07 +0900 Subject: [PATCH 1/8] Support generating development token for a development environment (#888) * new codegen * Imlpement token utils and unit tests * Implement devToken --------- Co-authored-by: Tommaso Barbugli Co-authored-by: Thierry Schellenbach --- README.md | 1 + .../ui/components/avatar/UserAvatar.kt | 3 +- .../api/stream-video-android-core.api | 1 + .../video/android/core/StreamVideo.kt | 8 + .../video/android/core/utils/TokenUtils.kt | 69 ++++++++ .../android/core/utils/TokenUtilsTest.kt | 153 ++++++++++++++++++ 6 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/TokenUtils.kt create mode 100644 stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/utils/TokenUtilsTest.kt diff --git a/README.md b/README.md index d879e38057..f433ff5e9d 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ Video roadmap and changelog is available [here](https://github.com/GetStream/pro ### 0.5.0 milestone +- [X] Development token to support a development environment - [ ] H264 workaround on Samsung 23? (see https://github.com/livekit/client-sdk-android/blob/main/livekit-android-sdk/src/main/java/io/livekit/android/webrtc/SimulcastVideoEncoderFactoryWrapper.kt#L34 and - https://github.com/react-native-webrtc/react-native-webrtc/issues/983#issuecomment-975624906) - [ ] Test coverage diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatar.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatar.kt index ed3191adda..d91a5c3578 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatar.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatar.kt @@ -44,7 +44,8 @@ import io.getstream.video.android.model.User * * Based on the state within the [User], we either show an image or their initials. * - * @param user The user whose avatar we want to show. + * @param userName The user name whose avatar we want to show. + * @param userImage The user image whose avatar we want to show. * @param modifier Modifier for styling. * @param shape The shape of the avatar. * @param textStyle The [TextStyle] that will be used for the initials. 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 5293159365..0794454784 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -729,6 +729,7 @@ public abstract interface class io/getstream/video/android/core/StreamVideo : io public abstract fun connectAsync (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun createDevice (Lio/getstream/android/push/PushDevice;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun deleteDevice (Lio/getstream/video/android/model/Device;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun devToken (Ljava/lang/String;)Ljava/lang/String; public abstract fun getContext ()Landroid/content/Context; public abstract fun getEdges (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getState ()Lio/getstream/video/android/core/ClientState; diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideo.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideo.kt index 780df16a5b..6ba52e539d 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideo.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideo.kt @@ -27,6 +27,7 @@ import io.getstream.video.android.core.model.QueriedCalls import io.getstream.video.android.core.model.QueriedMembers import io.getstream.video.android.core.model.SortField import io.getstream.video.android.core.notifications.NotificationHandler +import io.getstream.video.android.core.utils.TokenUtils import io.getstream.video.android.model.Device import io.getstream.video.android.model.User import kotlinx.coroutines.Deferred @@ -216,6 +217,13 @@ public interface StreamVideo : NotificationHandler { } } + /** + * Generate a developer token that can be used to connect users while the app is using a development environment. + * + * @param userId the desired id of the user to be connected. + */ + public fun devToken(userId: String): String = TokenUtils.devToken(userId) + public fun cleanup() } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/TokenUtils.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/TokenUtils.kt new file mode 100644 index 0000000000..c61d9c2cc2 --- /dev/null +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/TokenUtils.kt @@ -0,0 +1,69 @@ +/* + * 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. + */ + +package io.getstream.video.android.core.utils + +import android.util.Base64 +import io.getstream.log.taggedLogger +import org.json.JSONException +import org.json.JSONObject +import java.nio.charset.StandardCharsets + +internal object TokenUtils { + + val logger by taggedLogger("Video:TokenUtils") + + fun getUserId(token: String): String = try { + JSONObject( + token + .takeIf { it.contains(".") } + ?.split(".") + ?.getOrNull(1) + ?.let { + String( + Base64.decode( + it.toByteArray(StandardCharsets.UTF_8), + Base64.NO_WRAP, + ), + ) + } + ?: "", + ).optString("user_id") + } catch (e: JSONException) { + logger.e(e) { "Unable to obtain userId from JWT Token Payload" } + "" + } catch (e: IllegalArgumentException) { + logger.e(e) { "Unable to obtain userId from JWT Token Payload" } + "" + } + + /** + * Generate a developer token that can be used to connect users while the app is using a development environment. + * + * @param userId the desired id of the user to be connected. + */ + fun devToken(userId: String): String { + require(userId.isNotEmpty()) { "User id must not be empty" } + val header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" // {"alg": "HS256", "typ": "JWT"} + val devSignature = "devtoken" + val payload: String = + Base64.encodeToString( + "{\"user_id\":\"$userId\"}".toByteArray(StandardCharsets.UTF_8), + Base64.NO_WRAP, + ) + return "$header.$payload.$devSignature" + } +} diff --git a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/utils/TokenUtilsTest.kt b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/utils/TokenUtilsTest.kt new file mode 100644 index 0000000000..8816cf21f7 --- /dev/null +++ b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/utils/TokenUtilsTest.kt @@ -0,0 +1,153 @@ +/* + * 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. + */ + +package io.getstream.video.android.core.utils + +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.ParameterizedRobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.test.assertEquals + +@RunWith(ParameterizedRobolectricTestRunner::class) +@Config(manifest = Config.NONE) +internal class TokenUtilsTest( + private val token: String, + private val expectedUserId: String, +) { + + @Test + fun `Should return userId inside of the token`() { + assertEquals(TokenUtils.getUserId(token), expectedUserId) + } + + companion object { + + @Suppress("MaxLineLength") + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters(name = "{index}: {0} => {1}") + fun data(): Collection> = listOf( + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiamMifQ==.devtoken", + "jc", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0=.devtoken", + "vishal", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYW1pbiJ9.devtoken", + "amin", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMWYzN2U1OGQtZDhiMC00NzZhLWE0ZjItZjg2MTFlMGQ4NWQ5In0.l3u9P1NKhJ91rI1tzOcABGh045Kj69-iVkC2yUtohVw", + "1f37e58d-d8b0-476a-a4f2-f8611e0d85d9", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiRnJhIn0.ENQGHEsAL3WjVhd_qTiJa_9ojGKi2ftJ8xlocT8SVX4", + "Fra", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNmQ5NTI3M2ItMzNmMC00MGY1LWIwN2MtMGRhMjYxMDkyMDc0In0.lT5O4EmWzhRKPTau6dHP4F6M42EA2aN_8-iAPuiFPLc", + "6d95273b-33f0-40f5-b07c-0da261092074", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMWUzMzAxMTEtNjcwZC00OWE3LThmMDgtZTY3MzQzMzhjNjQxIn0.YEFdEMWj5rurQKr0QMrvO72jGZHU-AlpUIbyY4jxYdU", + "1e330111-670d-49a7-8f08-e6734338c641", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMjllNDZkZWYtODhmNC00YjZhLWExMGMtNTg0ZDEwYzRmZGM5In0.Mxr4Prnb1-EVM5NSSP2EugLApSChoKnVFwe7ZO15V_U", + "29e46def-88f4-4b6a-a10c-584d10c4fdc9", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMWYwNTJjMDgtZjY4Mi00YTgzLTg5NmMtOWYxOWE2OGJkMmJiIn0.L-cQ-DYubOzFpsg94OEwlTRYjat9G4cqfAgzBPALW0g", + "1f052c08-f682-4a83-896c-9f19a68bd2bb", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMGQzZTZlNjMtNjIwMC00ZGQxLWE4NDEtNDA1MDY2NDg5MWUyIn0.osFIgnle17f6yEkK7rPJguQaKhOiawAO3BylYaiRTqE", + "0d3e6e63-6200-4dd1-a841-4050664891e2", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMTJmYjBlZDktOTNkOC00OGE1LTk4ODUtMjhlNDFmMmU0YzQzIn0.t_oc_DEwTav7ni0z4bi8Xla_5Zj5cI6l3rKxwoCvtB0", + "12fb0ed9-93d8-48a5-9885-28e41f2e4c43", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNTUzMWE4Y2ItM2I4MS00YTU0LWI0MjQtN2FlNGUyN2JmOGJhIn0.PXkmukg3JU4igH_YUMr7WC7a1EcwKBr_C5V2ouBlmIs", + "5531a8cb-3b81-4a54-b424-7ae4e27bf8ba", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMDYzNTY1NjQtMTQ5Zi00YjJjLTg1MjUtZDIyMDU2ZmVjNDA0In0.R3-HY9Cno62yIhCjLXDBR8LF7y1udwX8m4LLNP2dIZo", + "06356564-149f-4b2c-8525-d22056fec404", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiYWQ3ZDkzMTQtNTA3MS00ZDYxLTk4YTEtZmZhNjQzY2U4MjRhIn0.iF4UWGFtX0eTAIBTCum7fjD_TKn8wjEqb3PVxJrwbuM", + "ad7d9314-5071-4d61-98a1-ffa643ce824a", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY2ViZjU2MmEtNDgwNi00YzY0LWE4MjctNTlkNTBhYWM0MmJhIn0.kuXab7RhQRHdsErEW5tTN_mmuyLPNU4ZbprvuPXM4OY", + "cebf562a-4806-4c64-a827-59d50aac42ba", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MCJ9.Vow00KvvhLvWRZIPKomXQOYpBL_P-_-eDeDKmBRvEj4", + "qatest0", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MSJ9.H1nlYibjgp1HfaOd0sA_T4038tjsN61mJWxvUjmRQI0", + "qatest1", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MiJ9.GYp9ikLtU2eG9Mq7tmHThzbV7C8W82j18sExuO7-ogc", + "qatest2", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MyJ9.kLZJz5kl7e3Zw7i2T39Yp05_nAmh9RGG0rt6-5zOpfE", + "qatest3", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.kLZJz5kl7e3Zw7i2T39Yp05_nAmh9RGG0rt6-5zOpfE", + "", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", + "", + ), + arrayOf( + randomString(), + "", + ), + arrayOf( + "${randomString()}.", + "", + ), + arrayOf( + "${randomString()}.${randomString()}", + "", + ), + arrayOf( + "", + "", + ), + ) + } +} + +private val charPool: CharArray = (('a'..'z') + ('A'..'Z') + ('0'..'9')).toCharArray() + +private fun randomString(size: Int = 20): String = buildString(capacity = size) { + repeat(size) { + append(charPool.random()) + } +} From ec7b55be9f59cc8d96d04647d97f2906402d23c2 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Wed, 25 Oct 2023 13:20:41 +0200 Subject: [PATCH 2/8] Participant view redesign (#887) Redesign the participants grid view --- .../api/stream-video-android-compose.api | 55 +++- .../android/compose/theme/StreamColors.kt | 64 +++- .../android/compose/theme/StreamDimens.kt | 64 ++-- .../android/compose/theme/StreamShapes.kt | 11 +- .../components/avatar/UserAvatarBackground.kt | 51 ++-- .../call/renderer/ParticipantVideo.kt | 62 ++-- .../call/renderer/VideoRendererStyle.kt | 2 +- .../LandscapeScreenSharingVideoRenderer.kt | 25 +- .../internal/LandscapeVideoRenderer.kt | 252 +++++----------- .../renderer/internal/LazyRowVideoRenderer.kt | 4 +- .../PortraitScreenSharingVideoRenderer.kt | 89 ++++-- .../internal/PortraitVideoRenderer.kt | 283 +++++------------- .../renderer/internal/ScreenShareTooltip.kt | 5 +- .../connection/NetworkQualityIndicator.kt | 83 +---- .../connection/internal/ConnectionBars.kt | 97 ++++++ .../indicator/AudioVolumeIndicator.kt | 23 +- .../components/indicator/GenericIndicator.kt | 96 ++++++ .../indicator/MicrophoneIndicator.kt | 40 +-- .../ui/components/indicator/SoundIndicator.kt | 44 +-- .../src/main/res/values/colors.xml | 19 +- .../src/main/res/values/dimens.xml | 35 ++- 21 files changed, 728 insertions(+), 676 deletions(-) create mode 100644 stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/internal/ConnectionBars.kt create mode 100644 stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/GenericIndicator.kt 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 51493fca01..4d8cc319e0 100644 --- a/stream-video-android-compose/api/stream-video-android-compose.api +++ b/stream-video-android-compose/api/stream-video-android-compose.api @@ -58,7 +58,7 @@ public abstract interface class io/getstream/video/android/compose/state/ui/part public final class io/getstream/video/android/compose/theme/StreamColors { public static final field $stable I public static final field Companion Lio/getstream/video/android/compose/theme/StreamColors$Companion; - public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-0d7_KjU ()J public final fun component10-0d7_KjU ()J public final fun component11-0d7_KjU ()J @@ -90,19 +90,25 @@ public final class io/getstream/video/android/compose/theme/StreamColors { public final fun component35-0d7_KjU ()J public final fun component36-0d7_KjU ()J public final fun component37-0d7_KjU ()J + public final fun component38-0d7_KjU ()J + public final fun component39-0d7_KjU ()J public final fun component4-0d7_KjU ()J + public final fun component40-0d7_KjU ()J + public final fun component41-0d7_KjU ()J public final fun component5-0d7_KjU ()J public final fun component6-0d7_KjU ()J public final fun component7-0d7_KjU ()J public final fun component8-0d7_KjU ()J public final fun component9-0d7_KjU ()J - public final fun copy-siJM_ko (JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ)Lio/getstream/video/android/compose/theme/StreamColors; - public static synthetic fun copy-siJM_ko$default (Lio/getstream/video/android/compose/theme/StreamColors;JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIILjava/lang/Object;)Lio/getstream/video/android/compose/theme/StreamColors; + public final fun copy-ilA1nao (JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ)Lio/getstream/video/android/compose/theme/StreamColors; + public static synthetic fun copy-ilA1nao$default (Lio/getstream/video/android/compose/theme/StreamColors;JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIILjava/lang/Object;)Lio/getstream/video/android/compose/theme/StreamColors; public fun equals (Ljava/lang/Object;)Z public final fun getActivatedVolumeIndicator-0d7_KjU ()J public final fun getAppBackground-0d7_KjU ()J public final fun getAudioActionColor-0d7_KjU ()J + public final fun getAudioIndicatorBackground-0d7_KjU ()J public final fun getAudioLeaveButton-0d7_KjU ()J + public final fun getAvatarBorderColor-0d7_KjU ()J public final fun getAvatarInitials-0d7_KjU ()J public final fun getBarsBackground-0d7_KjU ()J public final fun getBorders-0d7_KjU ()J @@ -118,6 +124,7 @@ public final class io/getstream/video/android/compose/theme/StreamColors { public final fun getConnectionQualityBackground-0d7_KjU ()J public final fun getConnectionQualityBar-0d7_KjU ()J public final fun getConnectionQualityBarFilled-0d7_KjU ()J + public final fun getConnectionQualityBarFilledPoor-0d7_KjU ()J public final fun getDeActivatedVolumeIndicator-0d7_KjU ()J public final fun getDisabled-0d7_KjU ()J public final fun getErrorAccent-0d7_KjU ()J @@ -129,6 +136,7 @@ public final class io/getstream/video/android/compose/theme/StreamColors { public final fun getLiveIndicator-0d7_KjU ()J public final fun getOverlay-0d7_KjU ()J public final fun getOverlayDark-0d7_KjU ()J + public final fun getParticipantContainerBackground-0d7_KjU ()J public final fun getParticipantLabelBackground-0d7_KjU ()J public final fun getPrimaryAccent-0d7_KjU ()J public final fun getScreenSharingBackground-0d7_KjU ()J @@ -148,7 +156,7 @@ public final class io/getstream/video/android/compose/theme/StreamColors$Compani public final class io/getstream/video/android/compose/theme/StreamDimens { public static final field $stable I public static final field Companion Lio/getstream/video/android/compose/theme/StreamDimens$Companion; - public synthetic fun (FFFFFFFFFFFJJJJFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFFFFFJJJJFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-D9Ej5fM ()F public final fun component10-D9Ej5fM ()F public final fun component11-D9Ej5fM ()F @@ -230,9 +238,13 @@ public final class io/getstream/video/android/compose/theme/StreamDimens { public final fun component80-D9Ej5fM ()F public final fun component81-D9Ej5fM ()F public final fun component82-D9Ej5fM ()F + public final fun component83-D9Ej5fM ()F + public final fun component84-D9Ej5fM ()F + public final fun component85-D9Ej5fM ()F + public final fun component86-D9Ej5fM ()F public final fun component9-D9Ej5fM ()F - public final fun copy-uuJv5Cc (FFFFFFFFFFFJJJJFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)Lio/getstream/video/android/compose/theme/StreamDimens; - public static synthetic fun copy-uuJv5Cc$default (Lio/getstream/video/android/compose/theme/StreamDimens;FFFFFFFFFFFJJJJFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIIILjava/lang/Object;)Lio/getstream/video/android/compose/theme/StreamDimens; + public final fun copy-lgJDAHA (FFFFFFFFFFFJJJJFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)Lio/getstream/video/android/compose/theme/StreamDimens; + public static synthetic fun copy-lgJDAHA$default (Lio/getstream/video/android/compose/theme/StreamDimens;FFFFFFFFFFFJJJJFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIIILjava/lang/Object;)Lio/getstream/video/android/compose/theme/StreamDimens; public fun equals (Ljava/lang/Object;)Z public final fun getAudioAvatarPadding-D9Ej5fM ()F public final fun getAudioAvatarSize-D9Ej5fM ()F @@ -246,6 +258,8 @@ public final class io/getstream/video/android/compose/theme/StreamDimens { public final fun getAudioRoomMicPadding-D9Ej5fM ()F public final fun getAudioRoomMicSize-D9Ej5fM ()F public final fun getAvatarAppbarPadding-D9Ej5fM ()F + public final fun getAvatarBorderPadding-D9Ej5fM ()F + public final fun getAvatarBorderWidth-D9Ej5fM ()F public final fun getButtonToggleOffAlpha ()F public final fun getButtonToggleOnAlpha ()F public final fun getCallAppBarCenterContentSpacingEnd-D9Ej5fM ()F @@ -273,6 +287,7 @@ public final class io/getstream/video/android/compose/theme/StreamDimens { public final fun getGroupCallUserNameTextSize-XSAIIZE ()J public final fun getHeaderElevation-D9Ej5fM ()F public final fun getIncomingCallOptionsBottomPadding-D9Ej5fM ()F + public final fun getIndicatorBackgroundSize-D9Ej5fM ()F public final fun getLandscapeControlActionsButtonSize-D9Ej5fM ()F public final fun getLandscapeControlActionsWidth-D9Ej5fM ()F public final fun getLandscapeTopAppBarHeight-D9Ej5fM ()F @@ -286,6 +301,7 @@ public final class io/getstream/video/android/compose/theme/StreamDimens { public final fun getOnCallStatusTextAlpha ()F public final fun getOnCallStatusTextSize-XSAIIZE ()J public final fun getOutgoingCallOptionsBottomPadding-D9Ej5fM ()F + public final fun getParticipantContentRadius-D9Ej5fM ()F public final fun getParticipantFocusedBorderWidth-D9Ej5fM ()F public final fun getParticipantInfoMenuAppBarHeight-D9Ej5fM ()F public final fun getParticipantInfoMenuOptionsHeight-D9Ej5fM ()F @@ -327,10 +343,13 @@ public final class io/getstream/video/android/compose/theme/StreamDimens$Compani public final class io/getstream/video/android/compose/theme/StreamShapes { public static final field $stable I public static final field Companion Lio/getstream/video/android/compose/theme/StreamShapes$Companion; - public fun (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)V + public fun (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)V public final fun component1 ()Landroidx/compose/ui/graphics/Shape; public final fun component10 ()Landroidx/compose/ui/graphics/Shape; public final fun component11 ()Landroidx/compose/ui/graphics/Shape; + public final fun component12 ()Landroidx/compose/ui/graphics/Shape; + public final fun component13 ()Landroidx/compose/ui/graphics/Shape; + public final fun component14 ()Landroidx/compose/ui/graphics/Shape; public final fun component2 ()Landroidx/compose/ui/graphics/Shape; public final fun component3 ()Landroidx/compose/ui/graphics/Shape; public final fun component4 ()Landroidx/compose/ui/graphics/Shape; @@ -339,8 +358,8 @@ public final class io/getstream/video/android/compose/theme/StreamShapes { public final fun component7 ()Landroidx/compose/ui/graphics/Shape; public final fun component8 ()Landroidx/compose/ui/graphics/Shape; public final fun component9 ()Landroidx/compose/ui/graphics/Shape; - public final fun copy (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)Lio/getstream/video/android/compose/theme/StreamShapes; - public static synthetic fun copy$default (Lio/getstream/video/android/compose/theme/StreamShapes;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;ILjava/lang/Object;)Lio/getstream/video/android/compose/theme/StreamShapes; + public final fun copy (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)Lio/getstream/video/android/compose/theme/StreamShapes; + public static synthetic fun copy$default (Lio/getstream/video/android/compose/theme/StreamShapes;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;ILjava/lang/Object;)Lio/getstream/video/android/compose/theme/StreamShapes; public fun equals (Ljava/lang/Object;)Z public final fun getAvatar ()Landroidx/compose/ui/graphics/Shape; public final fun getCallButton ()Landroidx/compose/ui/graphics/Shape; @@ -351,6 +370,9 @@ public final class io/getstream/video/android/compose/theme/StreamShapes { public final fun getConnectionQualityIndicator ()Landroidx/compose/ui/graphics/Shape; public final fun getDialog ()Landroidx/compose/ui/graphics/Shape; public final fun getFloatingParticipant ()Landroidx/compose/ui/graphics/Shape; + public final fun getIndicatorBackground ()Landroidx/compose/ui/graphics/Shape; + public final fun getParticipantContainerShape ()Landroidx/compose/ui/graphics/Shape; + public final fun getParticipantLabelShape ()Landroidx/compose/ui/graphics/Shape; public final fun getParticipantsInfoMenuButton ()Landroidx/compose/ui/graphics/Shape; public final fun getSoundIndicatorBar ()Landroidx/compose/ui/graphics/Shape; public fun hashCode ()I @@ -568,7 +590,7 @@ public final class io/getstream/video/android/compose/ui/components/avatar/Onlin } public final class io/getstream/video/android/compose/ui/components/avatar/UserAvatarBackgroundKt { - public static final fun UserAvatarBackground-GIi8pss (Ljava/lang/String;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;FFLandroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/layout/ContentScale;Ljava/lang/String;JJIILjava/lang/Integer;Landroidx/compose/runtime/Composer;III)V + public static final fun UserAvatarBackground-5WCoS_E (Ljava/lang/String;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;FFLandroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/layout/ContentScale;Ljava/lang/String;JJILjava/lang/Integer;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/video/android/compose/ui/components/avatar/UserAvatarKt { @@ -1138,6 +1160,19 @@ public final class io/getstream/video/android/compose/ui/components/indicator/Co public final fun getLambda-1$stream_video_android_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/video/android/compose/ui/components/indicator/ComposableSingletons$GenericIndicatorKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/indicator/ComposableSingletons$GenericIndicatorKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function3; + public static field lambda-3 Lkotlin/jvm/functions/Function3; + public static field lambda-4 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_compose_release ()Lkotlin/jvm/functions/Function3; + 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 class io/getstream/video/android/compose/ui/components/indicator/ComposableSingletons$MicrophoneIndicatorKt { public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/indicator/ComposableSingletons$MicrophoneIndicatorKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamColors.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamColors.kt index 5f5790c8c0..fb972a8963 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamColors.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamColors.kt @@ -65,6 +65,7 @@ public data class StreamColors( public val connectionQualityBackground: Color, public val connectionQualityBar: Color, public val connectionQualityBarFilled: Color, + public val connectionQualityBarFilledPoor: Color, public val participantLabelBackground: Color, public val infoMenuOverlayColor: Color, public val callFocusedBorder: Color, @@ -79,6 +80,9 @@ public data class StreamColors( public val audioLeaveButton: Color, public val audioActionColor: Color, public val liveIndicator: Color, + public val audioIndicatorBackground: Color, + public val avatarBorderColor: Color, + public val participantContainerBackground: Color, ) { public companion object { @@ -103,14 +107,16 @@ public data class StreamColors( errorAccent = colorResource(R.color.stream_video_error_accent), infoAccent = colorResource(R.color.stream_video_info_accent), highlight = colorResource(R.color.stream_video_highlight), - avatarInitials = colorResource(id = R.color.stream_video_text_avatar_initials), - screenSharingBackground = colorResource(R.color.stream_video_app_background), + screenSharingBackground = colorResource( + R.color.stream_video_participant_container_background, + ), screenSharingTooltipBackground = colorResource( R.color.stream_video_screen_sharing_tooltip_background, ), screenSharingTooltipContent = colorResource( id = R.color.stream_video_screen_sharing_tooltip_content, ), + avatarInitials = colorResource(id = R.color.stream_video_text_avatar_initials), activatedVolumeIndicator = colorResource(id = R.color.stream_video_primary_accent), deActivatedVolumeIndicator = colorResource( id = R.color.stream_video_deactivated_volume_indicator, @@ -118,9 +124,14 @@ public data class StreamColors( connectionQualityBackground = colorResource( id = R.color.stream_video_connection_quality_background, ), - connectionQualityBarFilled = colorResource(id = R.color.stream_video_primary_accent), connectionQualityBar = colorResource( - id = R.color.stream_video_connection_quality_bar_background, + id = R.color.stream_video_connection_indicator_good, + ), + connectionQualityBarFilled = colorResource( + id = R.color.stream_video_connection_indicator_great, + ), + connectionQualityBarFilledPoor = colorResource( + id = R.color.stream_video_connection_indicator_poor, ), participantLabelBackground = colorResource( id = R.color.stream_video_participant_label_background, @@ -130,18 +141,25 @@ public data class StreamColors( callGradientStart = colorResource(id = R.color.stream_video_call_gradient_start), callGradientEnd = colorResource(id = R.color.stream_video_call_gradient_end), callDescription = colorResource(id = R.color.stream_video_call_description), - callActionIconEnabled = colorResource(id = R.color.stream_video_action_icon_enabled), - callActionIconDisabled = colorResource(id = R.color.stream_video_action_icon_disabled), callActionIconEnabledBackground = colorResource( id = R.color.stream_video_action_icon_enabled_background, ), callActionIconDisabledBackground = colorResource( id = R.color.stream_video_action_icon_disabled_background, ), + callActionIconEnabled = colorResource(id = R.color.stream_video_action_icon_enabled), + callActionIconDisabled = colorResource(id = R.color.stream_video_action_icon_disabled), callLobbyBackground = colorResource(id = R.color.stream_video_lobby_background), audioLeaveButton = colorResource(id = R.color.stream_video_audio_leave), audioActionColor = colorResource(id = R.color.stream_video_audio_room_actions), liveIndicator = colorResource(id = R.color.stream_video_live_indicator), + audioIndicatorBackground = colorResource( + id = R.color.stream_video_volume_indicator_background, + ), + avatarBorderColor = colorResource(id = R.color.stream_video_avatar_border_color), + participantContainerBackground = colorResource( + id = R.color.stream_video_participant_container_background, + ), ) /** @@ -165,7 +183,9 @@ public data class StreamColors( errorAccent = colorResource(R.color.stream_video_error_accent_dark), infoAccent = colorResource(R.color.stream_video_info_accent_dark), highlight = colorResource(R.color.stream_video_highlight_dark), - screenSharingBackground = colorResource(R.color.stream_video_app_background_dark), + screenSharingBackground = colorResource( + R.color.stream_video_participant_container_background, + ), screenSharingTooltipBackground = colorResource( R.color.stream_video_screen_sharing_tooltip_background, ), @@ -180,34 +200,46 @@ public data class StreamColors( connectionQualityBackground = colorResource( id = R.color.stream_video_connection_quality_background, ), - connectionQualityBarFilled = colorResource(id = R.color.stream_video_primary_accent), connectionQualityBar = colorResource( - id = R.color.stream_video_connection_quality_bar_background, + id = R.color.stream_video_connection_indicator_good, + ), + connectionQualityBarFilled = colorResource( + id = R.color.stream_video_connection_indicator_great, + ), + connectionQualityBarFilledPoor = colorResource( + id = R.color.stream_video_connection_indicator_poor, ), participantLabelBackground = colorResource( - id = R.color.stream_video_participant_label_background, + id = R.color.stream_video_participant_label_background_dark, ), infoMenuOverlayColor = Color.LightGray.copy(alpha = 0.7f), callFocusedBorder = colorResource(id = R.color.stream_video_focused_border_color), callGradientStart = colorResource(id = R.color.stream_video_call_gradient_start), callGradientEnd = colorResource(id = R.color.stream_video_call_gradient_end), callDescription = colorResource(id = R.color.stream_video_call_description_dark), - callActionIconEnabled = colorResource( - id = R.color.stream_video_action_icon_enabled_dark, - ), - callActionIconDisabled = colorResource( - id = R.color.stream_video_action_icon_disabled_dark, - ), callActionIconEnabledBackground = colorResource( id = R.color.stream_video_action_icon_enabled_background_dark, ), callActionIconDisabledBackground = colorResource( id = R.color.stream_video_action_icon_disabled_background_dark, ), + callActionIconEnabled = colorResource( + id = R.color.stream_video_action_icon_enabled_dark, + ), + callActionIconDisabled = colorResource( + id = R.color.stream_video_action_icon_disabled_dark, + ), callLobbyBackground = colorResource(id = R.color.stream_video_lobby_background_dark), audioLeaveButton = colorResource(id = R.color.stream_video_audio_leave_dark), audioActionColor = colorResource(id = R.color.stream_video_audio_room_actions_dark), liveIndicator = colorResource(id = R.color.stream_video_live_indicator_dark), + audioIndicatorBackground = colorResource( + id = R.color.stream_video_volume_indicator_background_dark, + ), + avatarBorderColor = colorResource(id = R.color.stream_video_avatar_border_color), + participantContainerBackground = colorResource( + id = R.color.stream_video_participant_container_background, + ), ) } } diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamDimens.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamDimens.kt index 3fe0344714..9ff0a45659 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamDimens.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamDimens.kt @@ -81,6 +81,7 @@ public data class StreamDimens( public val participantsInfoMenuOptionsButtonHeight: Dp, public val participantsInfoAvatarSize: Dp, public val participantsGridPadding: Dp, + public val participantContentRadius: Dp, public val floatingVideoPadding: Dp, public val floatingVideoHeight: Dp, public val floatingVideoWidth: Dp, @@ -115,6 +116,9 @@ public data class StreamDimens( public val audioRoomMicPadding: Dp, public val audioRoomAvatarPortraitPadding: Dp, public val audioRoomAvatarLandscapePadding: Dp, + public val indicatorBackgroundSize: Dp, + public val avatarBorderPadding: Dp, + val avatarBorderWidth: Dp, ) { public companion object { /** @@ -141,7 +145,6 @@ public data class StreamDimens( participantsTextPadding = dimensionResource( id = R.dimen.stream_video_participantsTextPadding, ), - topAppbarTextSize = textSizeResource(id = R.dimen.stream_video_topAppbarTextSize), directCallUserNameTextSize = textSizeResource( id = R.dimen.stream_video_directCallUserNameTextSize, ), @@ -149,6 +152,7 @@ public data class StreamDimens( id = R.dimen.stream_video_groupCallUserNameTextSize, ), onCallStatusTextSize = textSizeResource(id = R.dimen.stream_video_onCallStatusTextSize), + topAppbarTextSize = textSizeResource(id = R.dimen.stream_video_topAppbarTextSize), onCallStatusTextAlpha = floatResource(R.dimen.stream_video_onCallStatusTextAlpha), buttonToggleOnAlpha = floatResource(R.dimen.stream_video_buttonToggleOnAlpha), buttonToggleOffAlpha = floatResource(R.dimen.stream_video_buttonToggleOffAlpha), @@ -186,9 +190,24 @@ public data class StreamDimens( callAppBarRecordingIndicatorSize = dimensionResource( id = R.dimen.stream_video_callAppBarRecordingIndicatorSize, ), + controlActionsBottomPadding = dimensionResource( + id = R.dimen.stream_video_controlActionsBottomPadding, + ), + controlActionsHeight = dimensionResource( + id = R.dimen.stream_video_controlActionsHeight, + ), controlActionsButtonSize = dimensionResource( id = R.dimen.stream_video_controlActionsButtonSize, ), + controlActionsElevation = dimensionResource( + id = R.dimen.stream_video_controlActionsElevation, + ), + landscapeControlActionsWidth = dimensionResource( + id = R.dimen.stream_video_landscapeControlActionsWidth, + ), + landscapeControlActionsButtonSize = dimensionResource( + id = R.dimen.stream_video_landscapeControlActionsButtonSize, + ), participantFocusedBorderWidth = dimensionResource( id = R.dimen.stream_video_activeSpeakerBoarderWidth, ), @@ -210,21 +229,6 @@ public data class StreamDimens( participantLabelTextPaddingStart = dimensionResource( id = R.dimen.stream_video_callParticipantSoundIndicatorPaddingStart, ), - participantsGridPadding = dimensionResource( - id = R.dimen.stream_video_participantsGridPadding, - ), - landscapeControlActionsButtonSize = dimensionResource( - id = R.dimen.stream_video_landscapeControlActionsButtonSize, - ), - controlActionsHeight = dimensionResource( - id = R.dimen.stream_video_controlActionsHeight, - ), - controlActionsElevation = dimensionResource( - id = R.dimen.stream_video_controlActionsElevation, - ), - landscapeControlActionsWidth = dimensionResource( - id = R.dimen.stream_video_landscapeControlActionsWidth, - ), participantInfoMenuAppBarHeight = dimensionResource( id = R.dimen.stream_video_participantInfoMenuAppBarHeight, ), @@ -237,6 +241,12 @@ public data class StreamDimens( participantsInfoAvatarSize = dimensionResource( id = R.dimen.stream_video_participantsInfoAvatarSize, ), + participantsGridPadding = dimensionResource( + id = R.dimen.stream_video_participantsGridPadding, + ), + participantContentRadius = dimensionResource( + id = R.dimen.stream_video_callParticipant_container_radius, + ), floatingVideoPadding = dimensionResource( id = R.dimen.stream_video_floatingVideoPadding, ), @@ -260,15 +270,15 @@ public data class StreamDimens( audioLevelIndicatorBarSeparatorWidth = dimensionResource( id = R.dimen.stream_video_audioLevelIndicatorBarSeparatorWidth, ), + audioLevelIndicatorBarPadding = dimensionResource( + id = R.dimen.stream_video_audioLevelIndicatorBarPadding, + ), microphoneIndicatorSize = dimensionResource( id = R.dimen.stream_video_microphoneIndicatorSize, ), microphoneIndicatorPadding = dimensionResource( id = R.dimen.stream_video_microphoneIndicatorPadding, ), - audioLevelIndicatorBarPadding = dimensionResource( - id = R.dimen.stream_video_audioLevelIndicatorBarPadding, - ), screenShareParticipantItemSize = dimensionResource( id = R.dimen.stream_video_screenShareParticipantItemSize, ), @@ -287,18 +297,15 @@ public data class StreamDimens( screenShareParticipantsRadius = dimensionResource( id = R.dimen.stream_video_screenShareParticipantsRadius, ), + screenSharePresenterPadding = dimensionResource( + id = R.dimen.stream_video_screenSharePresenterPadding, + ), screenSharePresenterTooltipMargin = dimensionResource( id = R.dimen.stream_video_screenSharePresenterTooltipMargin, ), screenSharePresenterTooltipPadding = dimensionResource( id = R.dimen.stream_video_screenSharePresenterTooltipPadding, ), - screenSharePresenterPadding = dimensionResource( - id = R.dimen.stream_video_screenSharePresenterPadding, - ), - controlActionsBottomPadding = dimensionResource( - id = R.dimen.stream_video_controlActionsBottomPadding, - ), screenSharePresenterTooltipIconPadding = dimensionResource( id = R.dimen.stream_video_screenShareTooltipIconPadding, ), @@ -326,6 +333,13 @@ public data class StreamDimens( audioRoomAvatarLandscapePadding = dimensionResource( id = R.dimen.stream_video_audioRoomAvatarLandscapePadding, ), + indicatorBackgroundSize = dimensionResource( + id = R.dimen.stream_video_IndicatorBackgroundSize, + ), + avatarBorderPadding = dimensionResource( + id = R.dimen.stream_video_audioAvatarBorderPadding, + ), + avatarBorderWidth = dimensionResource(id = R.dimen.stream_video_avatarBorderWidth), ) } } diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamShapes.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamShapes.kt index bd702b0750..de740b6b28 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamShapes.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamShapes.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp * @param callControls The shape of the call controls sheet when in a call. * @param callControlsButton Tha shape of the buttons within Call Controls. * @param participantsInfoMenuButton The shape of buttons in the Participants Info menu. + * @param indicatorBackground The indicator background shape. */ @Immutable public data class StreamShapes( @@ -47,6 +48,9 @@ public data class StreamShapes( public val soundIndicatorBar: Shape, public val floatingParticipant: Shape, public val connectionQualityIndicator: Shape, + public val indicatorBackground: Shape, + val participantLabelShape: Shape, + val participantContainerShape: Shape, ) { public companion object { /** @@ -57,16 +61,19 @@ public data class StreamShapes( @Composable public fun defaultShapes(): StreamShapes = StreamShapes( avatar = CircleShape, + dialog = RoundedCornerShape(16.dp), callButton = CircleShape, callControls = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), callControlsLandscape = RectangleShape, callControlsButton = CircleShape, participantsInfoMenuButton = RoundedCornerShape(32.dp), - dialog = RoundedCornerShape(16.dp), connectionIndicatorBar = RoundedCornerShape(16.dp), soundIndicatorBar = RoundedCornerShape(16.dp), floatingParticipant = RoundedCornerShape(16.dp), - connectionQualityIndicator = RoundedCornerShape(5.dp), + connectionQualityIndicator = RoundedCornerShape(topStart = 5.dp), + indicatorBackground = RoundedCornerShape(5.dp), + participantLabelShape = RoundedCornerShape(topEnd = 5.dp), + participantContainerShape = RoundedCornerShape(16.dp), ) } } diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatarBackground.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatarBackground.kt index a6ea9b8e57..5af0a1b48d 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatarBackground.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatarBackground.kt @@ -17,8 +17,10 @@ package io.getstream.video.android.compose.ui.components.avatar import androidx.annotation.DrawableRes +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable @@ -35,7 +37,6 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.compose.ui.components.background.ParticipantImageBackground import io.getstream.video.android.mock.StreamMockUtils import io.getstream.video.android.mock.mockUsers @@ -62,40 +63,44 @@ public fun UserAvatarBackground( userImage: String?, modifier: Modifier = Modifier, shape: Shape = VideoTheme.shapes.avatar, - avatarSize: Dp = 72.dp, + avatarSize: Dp = 84.dp, avatarShadowElevation: Dp = 12.dp, textStyle: TextStyle = VideoTheme.typography.title3Bold, contentScale: ContentScale = ContentScale.Crop, contentDescription: String? = null, requestSize: IntSize = IntSize(DEFAULT_IMAGE_SIZE, DEFAULT_IMAGE_SIZE), initialsAvatarOffset: DpOffset = DpOffset(0.dp, 0.dp), - blurRadius: Int = 20, @DrawableRes previewPlaceholder: Int = LocalAvatarPreviewProvider.getLocalAvatarPreviewPlaceholder(), @DrawableRes loadingPlaceholder: Int? = LocalAvatarPreviewProvider.getLocalAvatarLoadingPlaceholder(), ) { - Box(modifier = modifier) { - ParticipantImageBackground( - modifier = Modifier.fillMaxSize(), - userImage = userImage, - blurRadius = blurRadius, - ) - - UserAvatar( + Box(modifier = modifier.fillMaxSize()) { + Box( modifier = Modifier .size(avatarSize) .align(Alignment.Center) - .shadow(elevation = avatarShadowElevation, shape = CircleShape), - userName = userName, - userImage = userImage, - shape = shape, - textStyle = textStyle, - contentScale = contentScale, - contentDescription = contentDescription, - requestSize = requestSize, - initialsAvatarOffset = initialsAvatarOffset, - previewPlaceholder = previewPlaceholder, - loadingPlaceholder = loadingPlaceholder, - ) + .border( + width = VideoTheme.dimens.avatarBorderWidth, + color = VideoTheme.colors.avatarBorderColor, + shape = VideoTheme.shapes.avatar, + ), + ) { + UserAvatar( + modifier = Modifier + .padding(VideoTheme.dimens.avatarBorderPadding) + .align(Alignment.Center) + .shadow(elevation = avatarShadowElevation, shape = CircleShape), + userName = userName, + userImage = userImage, + shape = shape, + textStyle = textStyle, + contentScale = contentScale, + contentDescription = contentDescription, + requestSize = requestSize, + initialsAvatarOffset = initialsAvatarOffset, + previewPlaceholder = previewPlaceholder, + loadingPlaceholder = loadingPlaceholder, + ) + } } } diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt index 56682b94d2..bf2dd9fb37 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt @@ -46,11 +46,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.BottomEnd import androidx.compose.ui.Alignment.Companion.BottomStart +import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode @@ -106,7 +106,9 @@ public fun ParticipantVideo( connectionIndicatorContent: @Composable BoxScope.(NetworkQuality) -> Unit = { NetworkQualityIndicator( networkQuality = it, - modifier = Modifier.align(BottomEnd), + modifier = Modifier + .align(BottomEnd) + .height(VideoTheme.dimens.participantLabelHeight), ) }, videoFallbackContent: @Composable (Call) -> Unit = { @@ -137,6 +139,11 @@ public fun ParticipantVideo( } } + val containerShape = if (style.isScreenSharing) { + RoundedCornerShape(VideoTheme.dimens.screenShareParticipantsRadius) + } else { + VideoTheme.shapes.participantContainerShape + } val containerModifier = if (style.isFocused && participants.size > 1) { modifier.border( border = if (style.isScreenSharing) { @@ -150,22 +157,15 @@ public fun ParticipantVideo( VideoTheme.colors.callFocusedBorder, ) }, - shape = if (style.isScreenSharing) { - RoundedCornerShape(VideoTheme.dimens.screenShareParticipantsRadius) - } else { - RectangleShape - }, + shape = containerShape, ) } else { modifier } - Box( - modifier = containerModifier.apply { - if (style.isScreenSharing) { - clip(RoundedCornerShape(VideoTheme.dimens.screenShareParticipantsRadius)) - } - }, + modifier = containerModifier + .clip(containerShape) + .background(VideoTheme.colors.participantContainerBackground), ) { ParticipantVideoRenderer( call = call, @@ -294,32 +294,34 @@ public fun BoxScope.ParticipantLabel( ) }, ) { - Row( + Box( modifier = Modifier .align(labelPosition) - .padding(VideoTheme.dimens.participantLabelPadding) .height(VideoTheme.dimens.participantLabelHeight) .wrapContentWidth() - .clip(RoundedCornerShape(8.dp)) .background( VideoTheme.colors.participantLabelBackground, - shape = RoundedCornerShape(8.dp), + shape = VideoTheme.shapes.participantLabelShape, ), - verticalAlignment = CenterVertically, ) { - Text( - modifier = Modifier - .widthIn(max = VideoTheme.dimens.participantLabelTextMaxWidth) - .padding(start = VideoTheme.dimens.participantLabelTextPaddingStart) - .align(CenterVertically), - text = nameLabel, - style = VideoTheme.typography.body, - color = Color.White, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + Row( + modifier = Modifier.align(Center), + verticalAlignment = CenterVertically, + ) { + Text( + modifier = Modifier + .widthIn(max = VideoTheme.dimens.participantLabelTextMaxWidth) + .padding(start = VideoTheme.dimens.participantLabelTextPaddingStart) + .align(CenterVertically), + text = nameLabel, + style = VideoTheme.typography.body, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) - soundIndicatorContent.invoke(this) + soundIndicatorContent.invoke(this) + } } } diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle.kt index 2fd3eb3b34..30041a95e5 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle.kt @@ -111,7 +111,7 @@ public data class RegularVideoRendererStyle( override val isShowingConnectionQualityIndicator: Boolean = true, override val labelPosition: Alignment = Alignment.BottomStart, override val reactionDuration: Int = 650, - override val reactionPosition: Alignment = Alignment.Center, + override val reactionPosition: Alignment = Alignment.TopEnd, ) : VideoRendererStyle( isFocused, diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeScreenSharingVideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeScreenSharingVideoRenderer.kt index f7c0dd89c0..d5e29f6b45 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeScreenSharingVideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeScreenSharingVideoRenderer.kt @@ -19,15 +19,16 @@ package io.getstream.video.android.compose.ui.components.call.renderer.internal import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview @@ -80,16 +81,16 @@ internal fun LandscapeScreenSharingVideoRenderer( val sharingParticipant = session.participant val me by call.state.me.collectAsStateWithLifecycle() - Row( - modifier = modifier - .fillMaxSize() - .background(VideoTheme.colors.screenSharingBackground), + Column( + modifier = modifier.fillMaxWidth(), ) { Box( modifier = Modifier - .fillMaxHeight() + .padding(VideoTheme.dimens.participantsGridPadding) + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() .weight(0.65f) - .padding(VideoTheme.dimens.participantsGridPadding), + .background(VideoTheme.colors.screenSharingBackground), ) { ScreenShareVideoRenderer( modifier = Modifier.fillMaxSize(), @@ -106,10 +107,8 @@ internal fun LandscapeScreenSharingVideoRenderer( } } - LazyColumnVideoRenderer( - modifier = Modifier - .width(156.dp) - .fillMaxHeight(), + LazyRowVideoRenderer( + modifier = Modifier.fillMaxWidth(), call = call, participants = participants, dominantSpeaker = dominantSpeaker, diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeVideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeVideoRenderer.kt index 334ac1e40b..2d4f24092b 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeVideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeVideoRenderer.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.runtime.Composable @@ -84,14 +85,17 @@ internal fun BoxScope.LandscapeVideoRenderer( }, ) { val remoteParticipants by call.state.remoteParticipants.collectAsStateWithLifecycle() - + val paddedModifier = modifier.padding(VideoTheme.dimens.participantsGridPadding) when (callParticipants.size) { - 0 -> Unit - 1 -> { - val participant = callParticipants.first() + 1, 2 -> { + val participant = if (remoteParticipants.isEmpty()) { + callParticipants.first() + } else { + remoteParticipants.first() + } videoRenderer.invoke( - modifier = Modifier.fillMaxHeight(), + modifier = paddedModifier.fillMaxHeight(), call = call, participant = participant, style = style.copy( @@ -100,13 +104,12 @@ internal fun BoxScope.LandscapeVideoRenderer( ) } - 2, 3 -> { + 3, 4 -> { val rowItemWeight = 1f / callParticipants.size - Row(modifier = modifier) { remoteParticipants.take(callParticipants.size - 1).forEach { participant -> videoRenderer.invoke( - modifier = Modifier + modifier = paddedModifier .fillMaxHeight() .weight(rowItemWeight), call = call, @@ -119,181 +122,28 @@ internal fun BoxScope.LandscapeVideoRenderer( } } - 4 -> { - val firstParticipant = callParticipants[0] - val secondParticipant = callParticipants[1] - val thirdParticipant = callParticipants[2] - val fourthParticipant = callParticipants[3] - - Column(modifier) { - Row(modifier = Modifier.weight(1f)) { - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = firstParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == firstParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = secondParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == secondParticipant.sessionId, - ), - - ) - } - - Row(modifier = Modifier.weight(1f)) { - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = thirdParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == thirdParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = fourthParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == fourthParticipant.sessionId, - ), - ) - } - } - } - - 5 -> { - val firstParticipant = callParticipants[0] - val secondParticipant = callParticipants[1] - val thirdParticipant = callParticipants[2] - val fourthParticipant = callParticipants[3] - val fifthParticipant = callParticipants[4] - - Column(modifier) { - Row(modifier = Modifier.weight(1f)) { - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = firstParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == firstParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = secondParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == secondParticipant.sessionId, - ), - ) - } - - Row(modifier = Modifier.weight(1f)) { - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = thirdParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == thirdParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = fourthParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == fourthParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = fifthParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == fifthParticipant.sessionId, - ), - ) - } - } - } - - 6 -> { - val firstParticipant = callParticipants[0] - val secondParticipant = callParticipants[1] - val thirdParticipant = callParticipants[2] - val fourthParticipant = callParticipants[3] - val fifthParticipant = callParticipants[4] - val sixthParticipant = callParticipants[5] - + 5, 6 -> { + val rowSize = if (callParticipants.size == 5) Pair(3, 2) else Pair(3, 3) Column(modifier) { - Row(modifier = Modifier.weight(1f)) { - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = firstParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == firstParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = secondParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == secondParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = thirdParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == thirdParticipant.sessionId, - ), - ) - } - - Row(modifier = Modifier.weight(1f)) { - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = fourthParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == fourthParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = fifthParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == fifthParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = sixthParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == sixthParticipant.sessionId, - ), - ) - } + ParticipantRow( + modifier = Modifier.weight(1f), + participants = callParticipants.take(rowSize.first), + videoRenderer = videoRenderer, + paddedModifier = paddedModifier, + call = call, + style = style, + dominantSpeaker = dominantSpeaker, + ) + ParticipantRow( + modifier = Modifier.weight(1f), + participants = callParticipants.takeLast(rowSize.second), + videoRenderer = videoRenderer, + paddedModifier = paddedModifier, + call = call, + style = style, + dominantSpeaker = dominantSpeaker, + expectedRowSize = rowSize.first, + ) } } @@ -315,7 +165,7 @@ internal fun BoxScope.LandscapeVideoRenderer( } val participant = callParticipants[key] videoRenderer.invoke( - modifier = modifier.height(itemHeight), + modifier = paddedModifier.height(itemHeight), call = call, participant = participant, style = style.copy( @@ -329,7 +179,7 @@ internal fun BoxScope.LandscapeVideoRenderer( } } - if (callParticipants.size in 2..3) { + if (callParticipants.size in 2..4) { val currentLocal by call.state.me.collectAsStateWithLifecycle() if (currentLocal != null || LocalInspectionMode.current) { @@ -347,6 +197,40 @@ internal fun BoxScope.LandscapeVideoRenderer( } } +@Composable +private fun ParticipantRow( + modifier: Modifier, + participants: List, + videoRenderer: @Composable ( + modifier: Modifier, + call: Call, + participant: ParticipantState, + style: VideoRendererStyle, + ) -> Unit, + paddedModifier: Modifier, + call: Call, + style: VideoRendererStyle, + dominantSpeaker: ParticipantState?, + expectedRowSize: Int = participants.size, +) { + Row(modifier) { + repeat(participants.size) { + val participant = participants[it] + videoRenderer.invoke( + modifier = paddedModifier.weight(1f), + call = call, + participant = participant, + style = style.copy( + isFocused = dominantSpeaker?.sessionId == participant.sessionId, + ), + ) + } + repeat(expectedRowSize - participants.size) { + Box(modifier = paddedModifier.weight(1f)) + } + } +} + @Preview(device = Devices.AUTOMOTIVE_1024p, widthDp = 1440, heightDp = 720) @Composable private fun LandscapeParticipantsPreview1() { diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyRowVideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyRowVideoRenderer.kt index 01c0f7ffac..faf05ee32b 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyRowVideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyRowVideoRenderer.kt @@ -115,9 +115,11 @@ private fun ListVideoRenderer( ) }, ) { + val height = VideoTheme.dimens.screenShareParticipantItemSize + val width = height * 1.5f videoRenderer.invoke( modifier = Modifier - .size(VideoTheme.dimens.screenShareParticipantItemSize) + .size(width, height) .clip(RoundedCornerShape(VideoTheme.dimens.screenShareParticipantsRadius)), call = call, participant = participant, diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt index 03a509ecdc..23c12243d4 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt @@ -19,23 +19,36 @@ package io.getstream.video.android.compose.ui.components.call.renderer.internal import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope 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.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.ScreenSharingVideoRendererStyle import io.getstream.video.android.compose.ui.components.call.renderer.VideoRendererStyle +import io.getstream.video.android.compose.ui.components.call.renderer.copy import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState import io.getstream.video.android.core.model.ScreenSharingSession @@ -78,15 +91,68 @@ internal fun PortraitScreenSharingVideoRenderer( ) { val sharingParticipant = session.participant val me by call.state.me.collectAsStateWithLifecycle() + var parentSize: IntSize by remember { mutableStateOf(IntSize(0, 0)) } + val paddedModifier = modifier.padding(VideoTheme.dimens.participantsGridPadding) + BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { + LazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + columns = GridCells.Fixed(2), + content = { + item(span = { GridItemSpan(2) }) { + ScreenSharingContent( + modifier, + call, + session, + isZoomable, + me, + sharingParticipant, + ) + } + items( + count = participants.size, + ) { key -> + // make 3 items exactly fit available height + val itemHeight = with(LocalDensity.current) { + (constraints.maxHeight / 6).toDp() + } + val participant = participants[key] + videoRenderer.invoke( + modifier = paddedModifier.height(itemHeight), + call = call, + participant = participant, + style = style.copy( + isFocused = dominantSpeaker?.sessionId == participant.sessionId, + ), + ) + } + }, + ) + } +} + +@Composable +private fun BoxWithConstraintsScope.ScreenSharingContent( + modifier: Modifier, + call: Call, + session: ScreenSharingSession, + isZoomable: Boolean, + me: ParticipantState?, + sharingParticipant: ParticipantState, +) { + val itemHeight = with(LocalDensity.current) { + ((constraints.maxHeight * 0.45).toInt()).toDp() + } Column( - modifier = modifier.background(VideoTheme.colors.screenSharingBackground), + modifier = modifier + .padding(VideoTheme.dimens.participantsGridPadding), ) { Box( modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(VideoTheme.colors.screenSharingBackground) .fillMaxWidth() - .weight(1f) - .padding(VideoTheme.dimens.participantsGridPadding), + .height(itemHeight), ) { ScreenShareVideoRenderer( modifier = Modifier.fillMaxWidth(), @@ -102,21 +168,6 @@ internal fun PortraitScreenSharingVideoRenderer( ) } } - - Spacer( - modifier = Modifier.height( - VideoTheme.dimens.screenShareParticipantsScreenShareListMargin, - ), - ) - - LazyRowVideoRenderer( - modifier = Modifier.height(VideoTheme.dimens.screenShareParticipantsRowHeight), - call = call, - dominantSpeaker = dominantSpeaker, - participants = participants, - style = style, - videoRenderer = videoRenderer, - ) } } diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt index 272503ddcf..bbaa376578 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.runtime.Composable @@ -89,12 +90,16 @@ internal fun BoxScope.PortraitVideoRenderer( return } + val paddedModifier = modifier.padding(VideoTheme.dimens.participantsGridPadding) when (callParticipants.size) { - 1 -> { - val participant = callParticipants.first() - + 1, 2 -> { + val participant = if (remoteParticipants.isEmpty()) { + callParticipants.first() + } else { + remoteParticipants.first() + } videoRenderer.invoke( - modifier = modifier, + modifier = paddedModifier, call = call, participant = participant, style = style.copy( @@ -103,221 +108,45 @@ internal fun BoxScope.PortraitVideoRenderer( ) } - 2 -> { - val participant = remoteParticipants.first() - - videoRenderer.invoke( - modifier = modifier, - call = call, - participant = participant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == participant.sessionId, - ), + 3, 4 -> { + ParticipantColumn( + modifier, + remoteParticipants, + videoRenderer, + paddedModifier, + call, + style, + dominantSpeaker, + 0, ) } - 3 -> { - val firstParticipant = remoteParticipants[0] - val secondParticipant = remoteParticipants[1] + 5, 6 -> { + val columnSize = if (callParticipants.size == 5) Pair(3, 2) else Pair(3, 3) - Column(modifier) { - videoRenderer.invoke( - modifier = Modifier.weight(1f), + Row(modifier) { + ParticipantColumn( + modifier = modifier.weight(1f), + remoteParticipants = callParticipants.take(columnSize.first), + videoRenderer = videoRenderer, + paddedModifier = paddedModifier, call = call, - participant = firstParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == firstParticipant.sessionId, - ), + style = style, + dominantSpeaker = dominantSpeaker, ) - videoRenderer.invoke( - modifier = Modifier.weight(1f), + ParticipantColumn( + modifier = modifier.weight(1f), + remoteParticipants = callParticipants.takeLast(columnSize.second), + videoRenderer = videoRenderer, + paddedModifier = paddedModifier, call = call, - participant = secondParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == secondParticipant.sessionId, - ), + style = style, + dominantSpeaker = dominantSpeaker, + expectedColumnSize = columnSize.first, ) } } - - 4 -> { - val firstParticipant = callParticipants[0] - val secondParticipant = callParticipants[1] - val thirdParticipant = callParticipants[2] - val fourthParticipant = callParticipants[3] - - Row(modifier) { - Column(modifier = Modifier.weight(1f)) { - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = firstParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == firstParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = secondParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == secondParticipant.sessionId, - ), - ) - } - - Column(modifier = Modifier.weight(1f)) { - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = thirdParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == thirdParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = fourthParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == fourthParticipant.sessionId, - ), - ) - } - } - } - - 5 -> { - val firstParticipant = callParticipants[0] - val secondParticipant = callParticipants[1] - val thirdParticipant = callParticipants[2] - val fourthParticipant = callParticipants[3] - val fifthParticipant = callParticipants[4] - - Row(modifier) { - Column(modifier = Modifier.weight(1f)) { - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = firstParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == firstParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = secondParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == secondParticipant.sessionId, - ), - ) - } - - Column(modifier = Modifier.weight(1f)) { - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = thirdParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == thirdParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = fourthParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == fourthParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = fifthParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == fifthParticipant.sessionId, - ), - ) - } - } - } - - 6 -> { - val firstParticipant = callParticipants[0] - val secondParticipant = callParticipants[1] - val thirdParticipant = callParticipants[2] - val fourthParticipant = callParticipants[3] - val fifthParticipant = callParticipants[4] - val sixthParticipant = callParticipants[5] - - Row(modifier) { - Column(modifier = Modifier.weight(1f)) { - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = firstParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == firstParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = secondParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == secondParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = thirdParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == thirdParticipant.sessionId, - ), - ) - } - - Column(modifier = Modifier.weight(1f)) { - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = fourthParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == fourthParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = fifthParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == fifthParticipant.sessionId, - ), - ) - - videoRenderer.invoke( - modifier = Modifier.weight(1f), - call = call, - participant = sixthParticipant, - style = style.copy( - isFocused = dominantSpeaker?.sessionId == sixthParticipant.sessionId, - ), - ) - } - } - } - else -> { BoxWithConstraints(modifier = Modifier.fillMaxSize()) { val gridState = lazyGridStateWithVisibilityNotification(call = call) @@ -336,7 +165,7 @@ internal fun BoxScope.PortraitVideoRenderer( } val participant = callParticipants[key] videoRenderer.invoke( - modifier = modifier.height(itemHeight), + modifier = paddedModifier.height(itemHeight), call = call, participant = participant, style = style.copy( @@ -350,7 +179,7 @@ internal fun BoxScope.PortraitVideoRenderer( } } - if (callParticipants.size in 2..3) { + if (callParticipants.size in 2..4) { val currentLocal by call.state.me.collectAsStateWithLifecycle() if (currentLocal != null || LocalInspectionMode.current) { @@ -368,6 +197,40 @@ internal fun BoxScope.PortraitVideoRenderer( } } +@Composable +private fun ParticipantColumn( + modifier: Modifier, + remoteParticipants: List, + videoRenderer: @Composable ( + modifier: Modifier, + call: Call, + participant: ParticipantState, + style: VideoRendererStyle, + ) -> Unit, + paddedModifier: Modifier, + call: Call, + style: VideoRendererStyle, + dominantSpeaker: ParticipantState?, + expectedColumnSize: Int = remoteParticipants.size, +) { + Column(modifier) { + repeat(remoteParticipants.size) { + val participant = remoteParticipants[it] + videoRenderer.invoke( + modifier = paddedModifier.weight(1f), + call = call, + participant = participant, + style = style.copy( + isFocused = dominantSpeaker?.sessionId == participant.sessionId, + ), + ) + } + repeat(expectedColumnSize - remoteParticipants.size) { + Box(modifier = paddedModifier.weight(1f)) + } + } +} + @Preview @Composable private fun PortraitParticipantsPreview1() { diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/ScreenShareTooltip.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/ScreenShareTooltip.kt index a71910d6e3..21d045f7e7 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/ScreenShareTooltip.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/ScreenShareTooltip.kt @@ -47,13 +47,12 @@ internal fun ScreenShareTooltip( Row( modifier = modifier - .padding(VideoTheme.dimens.screenSharePresenterTooltipMargin) .height(VideoTheme.dimens.screenSharePresenterTooltipHeight) .wrapContentWidth() - .clip(RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(bottomEnd = 8.dp)) .background( color = VideoTheme.colors.screenSharingTooltipBackground, - shape = RoundedCornerShape(8.dp), + shape = RoundedCornerShape(bottomEnd = 8.dp), ), verticalAlignment = Alignment.CenterVertically, ) { diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/NetworkQualityIndicator.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/NetworkQualityIndicator.kt index 5e3783c68d..ec2be4458c 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/NetworkQualityIndicator.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/NetworkQualityIndicator.kt @@ -16,20 +16,14 @@ package io.getstream.video.android.compose.ui.components.connection -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.ui.components.connection.internal.ConnectionBars +import io.getstream.video.android.compose.ui.components.connection.internal.barColorsFromQuality +import io.getstream.video.android.compose.ui.components.indicator.GenericIndicator import io.getstream.video.android.core.model.NetworkQuality import stream.video.sfu.models.ConnectionQuality @@ -44,69 +38,13 @@ public fun NetworkQualityIndicator( networkQuality: NetworkQuality, modifier: Modifier = Modifier, ) { - val quality = networkQuality.quality - - Box( - modifier = modifier - .padding(8.dp) - .background( - shape = VideoTheme.shapes.connectionQualityIndicator, - color = VideoTheme.colors.connectionQualityBackground, - ) - .padding(6.dp), + val colors = barColorsFromQuality(networkQuality) + GenericIndicator( + modifier = modifier, + shape = VideoTheme.shapes.connectionQualityIndicator, + backgroundColor = VideoTheme.colors.connectionQualityBackground, ) { - Row( - modifier = Modifier - .height(height = VideoTheme.dimens.connectionIndicatorBarMaxHeight) - .align(Alignment.Center), - verticalAlignment = Alignment.Bottom, - ) { - Spacer( - modifier = Modifier - .width(VideoTheme.dimens.connectionIndicatorBarWidth) - .fillMaxHeight(0.33f) - .background( - color = if (quality > 0.33f) { - VideoTheme.colors.connectionQualityBarFilled - } else { - VideoTheme.colors.errorAccent - }, - shape = VideoTheme.shapes.connectionIndicatorBar, - ), - ) - - Spacer(modifier = Modifier.width(3.dp)) - - Spacer( - modifier = Modifier - .width(VideoTheme.dimens.connectionIndicatorBarWidth) - .fillMaxHeight(fraction = 0.66f) - .background( - color = if (quality >= 0.66f) { - VideoTheme.colors.connectionQualityBarFilled - } else { - VideoTheme.colors.connectionQualityBar - }, - shape = VideoTheme.shapes.connectionIndicatorBar, - ), - ) - - Spacer(modifier = Modifier.width(3.dp)) - - Spacer( - modifier = Modifier - .width(VideoTheme.dimens.connectionIndicatorBarWidth) - .fillMaxHeight(fraction = 1f) - .background( - color = if (quality >= 1) { - VideoTheme.colors.connectionQualityBarFilled - } else { - VideoTheme.colors.connectionQualityBar - }, - shape = VideoTheme.shapes.connectionIndicatorBar, - ), - ) - } + ConnectionBars(colors = colors) } } @@ -115,6 +53,9 @@ public fun NetworkQualityIndicator( private fun ConnectionQualityIndicatorPreview() { VideoTheme { Row { + NetworkQualityIndicator( + networkQuality = NetworkQuality.UnSpecified(), + ) NetworkQualityIndicator( networkQuality = NetworkQuality.Poor(), ) diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/internal/ConnectionBars.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/internal/ConnectionBars.kt new file mode 100644 index 0000000000..7c20da0b88 --- /dev/null +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/internal/ConnectionBars.kt @@ -0,0 +1,97 @@ +/* + * 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. + */ + +package io.getstream.video.android.compose.ui.components.connection.internal + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.core.model.NetworkQuality + +@Composable +internal fun barColorsFromQuality( + networkQuality: NetworkQuality, +): Triple = when (networkQuality) { + is NetworkQuality.Excellent -> Triple( + VideoTheme.colors.connectionQualityBarFilled, + VideoTheme.colors.connectionQualityBarFilled, + VideoTheme.colors.connectionQualityBarFilled, + ) + is NetworkQuality.Good -> Triple( + VideoTheme.colors.connectionQualityBarFilled, + VideoTheme.colors.connectionQualityBarFilled, + VideoTheme.colors.connectionQualityBar, + ) + is NetworkQuality.Poor -> Triple( + VideoTheme.colors.connectionQualityBarFilledPoor, + VideoTheme.colors.connectionQualityBar, + VideoTheme.colors.connectionQualityBar, + ) + is NetworkQuality.UnSpecified -> Triple( + VideoTheme.colors.connectionQualityBar, + VideoTheme.colors.connectionQualityBar, + VideoTheme.colors.connectionQualityBar, + ) +} + +@Composable +internal fun ConnectionBars(modifier: Modifier = Modifier, colors: Triple) { + Row( + modifier = modifier + .padding(VideoTheme.dimens.connectionIndicatorBarWidth) + .height(height = VideoTheme.dimens.connectionIndicatorBarMaxHeight), + verticalAlignment = Alignment.Bottom, + ) { + Spacer( + modifier = Modifier + .width(VideoTheme.dimens.connectionIndicatorBarWidth) + .fillMaxHeight(0.4f) + .background( + color = colors.first, + shape = VideoTheme.shapes.connectionIndicatorBar, + ), + ) + Spacer(modifier = Modifier.width(VideoTheme.dimens.connectionIndicatorBarSeparatorWidth)) + Spacer( + modifier = Modifier + .width(VideoTheme.dimens.connectionIndicatorBarWidth) + .fillMaxHeight(fraction = 0.7f) + .background( + color = colors.second, + shape = VideoTheme.shapes.connectionIndicatorBar, + ), + ) + Spacer(modifier = Modifier.width(VideoTheme.dimens.connectionIndicatorBarSeparatorWidth)) + Spacer( + modifier = Modifier + .width(VideoTheme.dimens.connectionIndicatorBarWidth) + .fillMaxHeight(fraction = 1f) + .background( + color = colors.third, + shape = VideoTheme.shapes.connectionIndicatorBar, + ), + ) + } +} diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/AudioVolumeIndicator.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/AudioVolumeIndicator.kt index f2375f2c48..b6c5253bbd 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/AudioVolumeIndicator.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/AudioVolumeIndicator.kt @@ -52,21 +52,28 @@ public fun AudioVolumeIndicator( Row( modifier = modifier .height(height = VideoTheme.dimens.audioLevelIndicatorBarMaxHeight) - .padding(horizontal = 4.dp), - verticalAlignment = Alignment.Bottom, + .padding(horizontal = 2.dp), + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy( VideoTheme.dimens.audioLevelIndicatorBarSeparatorWidth, ), ) { repeat(3) { index -> + // First bar 60%, second 100%, third 33% val audioLevel = - if (index == 0 || index == 2) { - // Draw "fake" side bars that reach 70% of the middle bar, similar to - // what Google Meet is doing. - audioLevels * 0.7f - } else { - audioLevels + when (index) { + 0 -> { + audioLevels * 0.6f + } + + 2 -> { + audioLevels * 0.33f + } + + else -> { + audioLevels + } } Spacer( diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/GenericIndicator.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/GenericIndicator.kt new file mode 100644 index 0000000000..04a8e68932 --- /dev/null +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/GenericIndicator.kt @@ -0,0 +1,96 @@ +/* + * 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. + */ + +package io.getstream.video.android.compose.ui.components.indicator + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.getstream.video.android.compose.theme.VideoTheme + +/** + * A composable that wraps its content into a rounded semi-transparent background. + */ +@Composable +internal fun GenericIndicator( + modifier: Modifier = Modifier, + shape: Shape, + backgroundColor: Color, + content: @Composable BoxScope.() -> Unit, +) { + Box( + modifier = modifier.size(VideoTheme.dimens.indicatorBackgroundSize), + ) { + val backgroundModifier = modifier + .matchParentSize() + + // Ensure content is center aligned and padded + Box( + modifier = Modifier + .matchParentSize() + .background( + color = backgroundColor, + shape = shape, + ), + ) + Box( + modifier = Modifier + .align(Alignment.Center) + .padding(4.dp), + ) { + Box(modifier = Modifier.align(Alignment.Center)) { + content(this) + } + } + } +} + +@Preview +@Composable +private fun PreviewIndicatorBackground() { + VideoTheme { + Column { + GenericIndicator( + backgroundColor = VideoTheme.colors.audioIndicatorBackground, + shape = VideoTheme.shapes.indicatorBackground, + ) { + AudioVolumeIndicator(audioLevels = 0.5f) + } + GenericIndicator( + backgroundColor = VideoTheme.colors.audioIndicatorBackground, + shape = VideoTheme.shapes.indicatorBackground, + ) { + MicrophoneIndicator(isMicrophoneEnabled = false) + } + GenericIndicator( + backgroundColor = VideoTheme.colors.audioIndicatorBackground, + shape = VideoTheme.shapes.indicatorBackground, + ) { + MicrophoneIndicator(isMicrophoneEnabled = true) + } + } + } +} diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/MicrophoneIndicator.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/MicrophoneIndicator.kt index b7a09418e9..1353bed0be 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/MicrophoneIndicator.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/MicrophoneIndicator.kt @@ -16,11 +16,13 @@ package io.getstream.video.android.compose.ui.components.indicator +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.Icon import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource @@ -39,24 +41,26 @@ public fun MicrophoneIndicator( modifier: Modifier = Modifier, isMicrophoneEnabled: Boolean, ) { - if (isMicrophoneEnabled) { - Icon( - modifier = modifier - .size(VideoTheme.dimens.microphoneIndicatorSize) - .padding(end = VideoTheme.dimens.microphoneIndicatorPadding), - painter = painterResource(id = R.drawable.stream_video_ic_mic_on), - tint = Color.White, - contentDescription = "microphone enabled", - ) - } else { - Icon( - modifier = modifier - .size(VideoTheme.dimens.microphoneIndicatorSize) - .padding(end = VideoTheme.dimens.microphoneIndicatorPadding), - painter = painterResource(id = R.drawable.stream_video_ic_mic_off), - tint = VideoTheme.colors.errorAccent, - contentDescription = "microphone disabled", - ) + Box( + modifier = modifier + .size(VideoTheme.dimens.microphoneIndicatorSize) + .padding(VideoTheme.dimens.microphoneIndicatorPadding), + ) { + if (isMicrophoneEnabled) { + Icon( + modifier = Modifier.align(Alignment.Center), + painter = painterResource(id = R.drawable.stream_video_ic_mic_on), + tint = Color.White, + contentDescription = "microphone enabled", + ) + } else { + Icon( + modifier = Modifier.align(Alignment.Center), + painter = painterResource(id = R.drawable.stream_video_ic_mic_off), + tint = Color.White, + contentDescription = "microphone disabled", + ) + } } } diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/SoundIndicator.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/SoundIndicator.kt index 237182216c..18be871b1b 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/SoundIndicator.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/SoundIndicator.kt @@ -16,16 +16,12 @@ package io.getstream.video.android.compose.ui.components.indicator -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon +import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.ui.common.R /** * Used to indicate the sound state of a given participant. Either shows a mute icon or the sound @@ -34,7 +30,7 @@ import io.getstream.video.android.ui.common.R * @param modifier Modifier for styling. * @param isSpeaking Represents is user speaking or not. * @param isAudioEnabled Represents is audio enabled or not. - * @param audioLevels Indicates the audio level that will be drawn. + * @param audioLevel Indicates the audio level that will be drawn. */ @Composable public fun SoundIndicator( @@ -43,20 +39,19 @@ public fun SoundIndicator( isAudioEnabled: Boolean, audioLevel: Float, ) { - if (isAudioEnabled && isSpeaking) { - AudioVolumeIndicator( - modifier = modifier.padding(end = VideoTheme.dimens.audioLevelIndicatorBarPadding), - audioLevels = audioLevel, - ) - } else if (!isAudioEnabled) { - Icon( - modifier = modifier - .size(VideoTheme.dimens.microphoneIndicatorSize) - .padding(end = VideoTheme.dimens.microphoneIndicatorPadding), - painter = painterResource(id = R.drawable.stream_video_ic_mic_off), - tint = VideoTheme.colors.errorAccent, - contentDescription = null, - ) + GenericIndicator( + modifier = modifier, + backgroundColor = VideoTheme.colors.audioIndicatorBackground, + shape = VideoTheme.shapes.indicatorBackground, + ) { + if (isAudioEnabled && isSpeaking) { + AudioVolumeIndicator( + modifier = Modifier.align(Alignment.Center), + audioLevels = audioLevel, + ) + } else { + MicrophoneIndicator(isMicrophoneEnabled = isAudioEnabled) + } } } @@ -64,7 +59,7 @@ public fun SoundIndicator( @Composable private fun SoundIndicatorPreview() { VideoTheme { - Row { + Column { SoundIndicator( isSpeaking = true, isAudioEnabled = true, @@ -75,6 +70,11 @@ private fun SoundIndicatorPreview() { isAudioEnabled = false, audioLevel = 0.5f, ) + SoundIndicator( + isSpeaking = false, + isAudioEnabled = true, + audioLevel = 0.5f, + ) } } } diff --git a/stream-video-android-ui-common/src/main/res/values/colors.xml b/stream-video-android-ui-common/src/main/res/values/colors.xml index a5d17d5a48..ea2da16896 100644 --- a/stream-video-android-ui-common/src/main/res/values/colors.xml +++ b/stream-video-android-ui-common/src/main/res/values/colors.xml @@ -21,20 +21,22 @@ #B4B7BB #DBDDE1 #E9EAED - #F7F7F8 + #FFFFFF #FFFFFF #E9F1FF #80000000 #99000000 #005FFF - #D9303030 - #D9303030 + #A60C0D0E + #A60C0D0E #FFFFFF #D9000000 #FCFCFC #FF3742 #20E070 #005FFF + #123D82 + #1E262E #FBF4DD #FFFFFF #545A64 @@ -47,7 +49,11 @@ #4C525C #FFFFFF #1E262E - #FFFFFF + #005FFF + #A60C0D0E + #00E2A1 + #FFFFFF + #DC433B #FF3742 @@ -73,8 +79,11 @@ #4C525C #FFFFFF #FFFFFF - #FFFFFF + #005FFF + #A60C0D0E #FF3742 + #A60C0D0E + #A60C0D0E #FF8A65 diff --git a/stream-video-android-ui-common/src/main/res/values/dimens.xml b/stream-video-android-ui-common/src/main/res/values/dimens.xml index 9c5af8f358..f45ca126ae 100644 --- a/stream-video-android-ui-common/src/main/res/values/dimens.xml +++ b/stream-video-android-ui-common/src/main/res/values/dimens.xml @@ -53,20 +53,23 @@ 200dp 140dp 16dp - 14dp - 3dp - 3dp - 16dp - 3dp - 3dp - 15dp - 6dp - 4dp - 6dp - 4dp - 24dp + 16dp + 8dp + 1dp + 2dp + 22dp + 2dp + 1dp + 24dp + 18dp + 2dp + 2dp + 1dp + 2dp + 2dp + 32dp 8dp - 0dp + 4dp 8dp 64dp 4dp @@ -85,10 +88,12 @@ 280dp 12dp 32dp - 45dp + 24dp 44dp 84dp - 4dp + 8dp + 4dp + 5dp 20dp 5dp 22dp From f8506c315eee59897a9546fa16ee3fab7296fabd Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Wed, 25 Oct 2023 17:56:16 +0300 Subject: [PATCH 3/8] Implement direct calls with Stream Google users for staging app (#863) Add Direct Call screen and functionality --- dogfooding/build.gradle.kts | 5 + dogfooding/src/main/AndroidManifest.xml | 2 +- ...gCallActivity.kt => DirectCallActivity.kt} | 8 +- .../video/android/IncomingCallActivity.kt | 2 + .../android/data/dto/GetGoogleAccountsDto.kt | 54 ++++ .../repositories/GoogleAccountRepository.kt | 141 +++++++++ .../getstream/video/android/di/AppModule.kt | 8 + .../video/android/models/GoogleAccount.kt | 25 ++ .../video/android/models/StreamUser.kt | 25 ++ .../video/android/ui/DogfoodingNavHost.kt | 19 +- .../video/android/ui/join/CallJoinScreen.kt | 25 +- .../video/android/ui/login/FirebaseSignIn.kt | 42 --- .../video/android/ui/login/GoogleSignIn.kt | 56 ++++ .../android/ui/login/GoogleSignInLauncher.kt | 55 ++++ .../video/android/ui/login/LoginScreen.kt | 33 +- .../video/android/ui/login/LoginViewModel.kt | 39 ++- .../android/ui/outgoing/DebugCallScreen.kt | 143 --------- .../ui/outgoing/DirectCallJoinScreen.kt | 281 ++++++++++++++++++ .../ui/outgoing/DirectCallViewModel.kt | 92 ++++++ .../android/ui/theme/StreamImageButton.kt | 53 ++++ .../video/android/util/GoogleSignInHelper.kt | 36 +++ .../android/util/StreamVideoInitHelper.kt | 2 +- .../{UserIdGenerator.kt => UserIdHelper.kt} | 8 +- dogfooding/src/main/res/values/strings.xml | 3 + gradle/libs.versions.toml | 1 + 25 files changed, 903 insertions(+), 255 deletions(-) rename dogfooding/src/main/kotlin/io/getstream/video/android/{RingCallActivity.kt => DirectCallActivity.kt} (96%) create mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/data/dto/GetGoogleAccountsDto.kt create mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/data/repositories/GoogleAccountRepository.kt create mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/models/GoogleAccount.kt create mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/models/StreamUser.kt delete mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/FirebaseSignIn.kt create mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignIn.kt create mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignInLauncher.kt delete mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/ui/outgoing/DebugCallScreen.kt create mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt create mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallViewModel.kt create mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/ui/theme/StreamImageButton.kt create mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/util/GoogleSignInHelper.kt rename dogfooding/src/main/kotlin/io/getstream/video/android/util/{UserIdGenerator.kt => UserIdHelper.kt} (87%) diff --git a/dogfooding/build.gradle.kts b/dogfooding/build.gradle.kts index be88329662..54434df794 100644 --- a/dogfooding/build.gradle.kts +++ b/dogfooding/build.gradle.kts @@ -242,6 +242,8 @@ dependencies { implementation(libs.androidx.hilt.navigation) implementation(libs.landscapist.coil) implementation(libs.accompanist.permission) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.coil.compose) // hilt implementation(libs.hilt.android) @@ -251,6 +253,9 @@ dependencies { implementation(libs.firebase.crashlytics) implementation(libs.firebase.analytics) + // moshi + implementation(libs.moshi.kotlin) + // Play Install Referrer library - used to extract the meeting link from demo flow after install implementation(libs.play.install.referrer) diff --git a/dogfooding/src/main/AndroidManifest.xml b/dogfooding/src/main/AndroidManifest.xml index 019a480f28..98b0ab252a 100644 --- a/dogfooding/src/main/AndroidManifest.xml +++ b/dogfooding/src/main/AndroidManifest.xml @@ -93,7 +93,7 @@ , ): Intent { - return Intent(context, RingCallActivity::class.java).apply { + return Intent(context, DirectCallActivity::class.java).apply { putExtra(EXTRA_CID, callId) putExtra(EXTRA_MEMBERS_ARRAY, members.toTypedArray()) } diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/IncomingCallActivity.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/IncomingCallActivity.kt index 650a2ec5d7..02e83869b1 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/IncomingCallActivity.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/IncomingCallActivity.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint import io.getstream.result.Result import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.call.activecall.CallContent @@ -46,6 +47,7 @@ import io.getstream.video.android.util.StreamVideoInitHelper import kotlinx.coroutines.launch import javax.inject.Inject +@AndroidEntryPoint class IncomingCallActivity : ComponentActivity() { @Inject diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/data/dto/GetGoogleAccountsDto.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/data/dto/GetGoogleAccountsDto.kt new file mode 100644 index 0000000000..bfec26d004 --- /dev/null +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/data/dto/GetGoogleAccountsDto.kt @@ -0,0 +1,54 @@ +/* + * 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. + */ + +package io.getstream.video.android.data.dto + +import io.getstream.video.android.models.GoogleAccount +import io.getstream.video.android.util.UserIdHelper +import java.util.Locale + +data class GetGoogleAccountsResponseDto( + val people: List, +) + +data class GoogleAccountDto( + val photos: List?, + val emailAddresses: List, +) + +data class PhotoDto( + val url: String, +) + +data class EmailAddressDto( + val value: String, +) + +fun GoogleAccountDto.asDomainModel(): GoogleAccount { + val email = emailAddresses.firstOrNull()?.value + + return GoogleAccount( + email = email, + id = email?.let { UserIdHelper.getUserIdFromEmail(it) }, + name = email + ?.split("@") + ?.firstOrNull() + ?.split(".") + ?.firstOrNull() + ?.capitalize(Locale.ROOT) ?: email, + photoUrl = photos?.firstOrNull()?.url, + ) +} diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/data/repositories/GoogleAccountRepository.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/data/repositories/GoogleAccountRepository.kt new file mode 100644 index 0000000000..f6d74c2e86 --- /dev/null +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/data/repositories/GoogleAccountRepository.kt @@ -0,0 +1,141 @@ +/* + * 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. + */ + +package io.getstream.video.android.data.repositories + +import android.content.Context +import android.util.Log +import com.google.android.gms.auth.GoogleAuthUtil +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.common.api.ApiException +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import dagger.hilt.android.qualifiers.ApplicationContext +import io.getstream.video.android.data.dto.GetGoogleAccountsResponseDto +import io.getstream.video.android.data.dto.asDomainModel +import io.getstream.video.android.models.GoogleAccount +import io.getstream.video.android.util.GoogleSignInHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import javax.inject.Inject + +class GoogleAccountRepository @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val baseUrl = "https://people.googleapis.com/v1/people:listDirectoryPeople" + + suspend fun getAllAccounts(): List? { + val readMask = "readMask=emailAddresses,names,photos" + val sources = "sources=DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE" + val pageSize = "pageSize=1000" + + return if (silentSignIn()) { + GoogleSignIn.getLastSignedInAccount(context)?.let { account -> + withContext(Dispatchers.IO) { + getAccessToken(account)?.let { accessToken -> + val urlString = "$baseUrl?access_token=$accessToken&$readMask&$sources&$pageSize" + val request = buildRequest(urlString) + val okHttpClient = buildOkHttpClient() + var responseBody: String? + + okHttpClient.newCall(request).execute().let { response -> + if (response.isSuccessful) { + responseBody = response.body?.string() + responseBody?.let { parseUserListJson(it) } + } else { + null + } + } + } + } + } + } else { + null + } + } + + private fun silentSignIn(): Boolean { // Used to refresh token + val gsc = GoogleSignInHelper.getGoogleSignInClient(context) + val task = gsc.silentSignIn() + + // Below code needed for debugging silent sign-in failures + if (task.isSuccessful) { + Log.d("Google Silent Sign In", "Successful") + return true + } else { + task.addOnCompleteListener { + try { + val signInAccount = task.getResult(ApiException::class.java) + Log.d("Google Silent Sign In", signInAccount.email.toString()) + } catch (apiException: ApiException) { + // You can get from apiException.getStatusCode() the detailed error code + // e.g. GoogleSignInStatusCodes.SIGN_IN_REQUIRED means user needs to take + // explicit action to finish sign-in; + // Please refer to GoogleSignInStatusCodes Javadoc for details + Log.d("Google Silent Sign In", apiException.statusCode.toString()) + } + } + return false + } + } + + private fun getAccessToken(account: GoogleSignInAccount) = + try { + GoogleAuthUtil.getToken( + context, + account.account, + "oauth2:profile email", + ) + } catch (e: Exception) { + null + } + + private fun buildRequest(urlString: String) = Request.Builder() + .url(urlString.toHttpUrl()) + .build() + + private fun buildOkHttpClient() = OkHttpClient.Builder() + .retryOnConnectionFailure(true) + .build() + + @OptIn(ExperimentalStdlibApi::class) + private fun parseUserListJson(jsonString: String): List? { + val moshi: Moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + val jsonAdapter: JsonAdapter = moshi.adapter() + + val response = jsonAdapter.fromJson(jsonString) + return response?.people?.map { it.asDomainModel() } + } + + fun getCurrentUser(): GoogleAccount { + val currentUser = GoogleSignIn.getLastSignedInAccount(context) + return GoogleAccount( + email = currentUser?.email ?: "", + id = currentUser?.id ?: "", + name = currentUser?.givenName ?: "", + photoUrl = currentUser?.photoUrl?.toString(), + isFavorite = false, + ) + } +} diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/di/AppModule.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/di/AppModule.kt index f34c1e8acc..fe3e20bca3 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/di/AppModule.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/di/AppModule.kt @@ -16,9 +16,12 @@ package io.getstream.video.android.di +import android.content.Context import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import io.getstream.video.android.data.repositories.GoogleAccountRepository import io.getstream.video.android.datastore.delegate.StreamUserDataStore import javax.inject.Singleton @@ -31,4 +34,9 @@ object AppModule { fun provideUserDataStore(): StreamUserDataStore { return StreamUserDataStore.instance() } + + @Provides + fun provideGoogleAccountRepository( + @ApplicationContext context: Context, + ) = GoogleAccountRepository(context) } diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/models/GoogleAccount.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/models/GoogleAccount.kt new file mode 100644 index 0000000000..befc88a1d4 --- /dev/null +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/models/GoogleAccount.kt @@ -0,0 +1,25 @@ +/* + * 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. + */ + +package io.getstream.video.android.models + +data class GoogleAccount( + val email: String?, + val id: String?, + val name: String?, + val photoUrl: String?, + val isFavorite: Boolean = false, +) diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/models/StreamUser.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/models/StreamUser.kt new file mode 100644 index 0000000000..1dbb21bc8a --- /dev/null +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/models/StreamUser.kt @@ -0,0 +1,25 @@ +/* + * 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. + */ + +package io.getstream.video.android.models + +data class StreamUser( + val email: String, + val id: String, + val name: String, + val avatarUrl: String?, + val isFavorite: Boolean, +) diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt index 213a75be88..5340646d34 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt @@ -26,11 +26,11 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import io.getstream.video.android.RingCallActivity +import io.getstream.video.android.DirectCallActivity import io.getstream.video.android.ui.join.CallJoinScreen import io.getstream.video.android.ui.lobby.CallLobbyScreen import io.getstream.video.android.ui.login.LoginScreen -import io.getstream.video.android.ui.outgoing.DebugCallScreen +import io.getstream.video.android.ui.outgoing.DirectCallJoinScreen @Composable fun AppNavHost( @@ -62,8 +62,8 @@ fun AppNavHost( popUpTo(AppScreens.CallJoin.destination) { inclusive = true } } }, - navigateToRingTest = { - navController.navigate(AppScreens.DebugCall.destination) + navigateToDirectCallJoin = { + navController.navigate(AppScreens.DirectCallJoin.destination) }, ) } @@ -79,15 +79,14 @@ fun AppNavHost( }, ) } - composable(AppScreens.DebugCall.destination) { + composable(AppScreens.DirectCallJoin.destination) { val context = LocalContext.current - DebugCallScreen( - navigateToRingCall = { callId, members -> + DirectCallJoinScreen( + navigateToDirectCall = { members -> context.startActivity( - RingCallActivity.createIntent( + DirectCallActivity.createIntent( context, members = members.split(","), - callId = callId, ), ) }, @@ -100,5 +99,5 @@ enum class AppScreens(val destination: String) { Login("login"), CallJoin("call_join"), CallLobby("call_preview"), - DebugCall("debug_call"), + DirectCallJoin("direct_call_join"), } diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt index d89102641a..0f0e384092 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt @@ -94,7 +94,7 @@ fun CallJoinScreen( callJoinViewModel: CallJoinViewModel = hiltViewModel(), navigateToCallLobby: (callId: String) -> Unit, navigateUpToLogin: () -> Unit, - navigateToRingTest: () -> Unit, + navigateToDirectCallJoin: () -> Unit, ) { val uiState by callJoinViewModel.uiState.collectAsState(CallJoinUiState.Nothing) val isLoggedOut by callJoinViewModel.isLoggedOut.collectAsState(initial = false) @@ -115,7 +115,7 @@ fun CallJoinScreen( ) { CallJoinHeader( callJoinViewModel = callJoinViewModel, - onRingTestClicked = navigateToRingTest, + onDirectCallClick = navigateToDirectCallJoin, ) CallJoinBody( @@ -144,7 +144,7 @@ fun CallJoinScreen( @Composable private fun CallJoinHeader( callJoinViewModel: CallJoinViewModel = hiltViewModel(), - onRingTestClicked: () -> Unit, + onDirectCallClick: () -> Unit, ) { val user by callJoinViewModel.user.collectAsState(initial = null) @@ -167,18 +167,21 @@ private fun CallJoinHeader( Text( modifier = Modifier.weight(1f), color = Color.White, - text = user?.name?.ifBlank { user?.id }?.ifBlank { user!!.custom["email"] } - .orEmpty(), + text = user?.name?.ifBlank { user?.id }?.ifBlank { user!!.custom["email"] }.orEmpty(), maxLines = 1, fontSize = 16.sp, ) if (BuildConfig.FLAVOR == "dogfooding") { - TextButton( - colors = ButtonDefaults.textButtonColors(contentColor = Color.White), - content = { Text(text = "Ring test") }, - onClick = { onRingTestClicked.invoke() }, - ) + if (user?.custom?.get("email")?.contains("getstreamio") == true) { + TextButton( + colors = ButtonDefaults.textButtonColors(contentColor = Color.White), + content = { Text(text = stringResource(R.string.direct_call)) }, + onClick = { onDirectCallClick.invoke() }, + ) + + Spacer(modifier = Modifier.width(5.dp)) + } StreamButton( modifier = Modifier.widthIn(125.dp), @@ -417,7 +420,7 @@ private fun CallJoinScreenPreview() { callJoinViewModel = CallJoinViewModel(StreamUserDataStore.instance()), navigateToCallLobby = {}, navigateUpToLogin = {}, - navigateToRingTest = {}, + navigateToDirectCallJoin = {}, ) } } diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/FirebaseSignIn.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/FirebaseSignIn.kt deleted file mode 100644 index 2bccfa3717..0000000000 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/FirebaseSignIn.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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. - */ - -package io.getstream.video.android.ui.login - -import androidx.activity.ComponentActivity -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.compose.runtime.Composable -import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract - -@Composable -fun rememberRegisterForActivityResult( - onSignInSuccess: (email: String) -> Unit, - onSignInFailed: () -> Unit, -) = rememberLauncherForActivityResult( - FirebaseAuthUIActivityResultContract(), -) { result -> - - if (result.resultCode != ComponentActivity.RESULT_OK) { - onSignInFailed.invoke() - } - - val email = result?.idpResponse?.email - if (email != null) { - onSignInSuccess(email) - } else { - onSignInFailed() - } -} diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignIn.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignIn.kt new file mode 100644 index 0000000000..f878a6a3ef --- /dev/null +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignIn.kt @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package io.getstream.video.android.ui.login + +import android.content.Intent +import androidx.activity.ComponentActivity +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.tasks.Task + +@Composable +fun rememberRegisterForActivityResult( + onSignInSuccess: (email: String) -> Unit, + onSignInFailed: () -> Unit, +): ManagedActivityResultLauncher { + return rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ) { result -> + if (result.resultCode != ComponentActivity.RESULT_OK) { + onSignInFailed.invoke() + } + + val task: Task = GoogleSignIn.getSignedInAccountFromIntent(result.data) + try { + val account = task.getResult(ApiException::class.java) + + account?.email?.let { + onSignInSuccess(it) + } ?: onSignInFailed() + } catch (e: ApiException) { + // The ApiException status code indicates the detailed failure reason. + // Please refer to the GoogleSignInStatusCodes class reference for more information. + onSignInFailed() + } + } +} diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignInLauncher.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignInLauncher.kt new file mode 100644 index 0000000000..091789d667 --- /dev/null +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignInLauncher.kt @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package io.getstream.video.android.ui.login + +import android.content.Intent +import androidx.activity.ComponentActivity +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.tasks.Task + +@Composable +fun rememberLauncherForGoogleSignInActivityResult( + onSignInSuccess: (email: String) -> Unit, + onSignInFailed: () -> Unit, +): ManagedActivityResultLauncher { + return rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ) { result -> + if (result.resultCode != ComponentActivity.RESULT_OK) { + onSignInFailed() + } else { + val task: Task = GoogleSignIn.getSignedInAccountFromIntent( + result.data, + ) + try { + val account = task.getResult(ApiException::class.java) + account?.email?.let { onSignInSuccess(it) } ?: onSignInFailed() + } catch (e: ApiException) { + // The ApiException status code indicates the detailed failure reason. + // Please refer to the GoogleSignInStatusCodes class reference for more information. + onSignInFailed() + } + } + } +} diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt index 7f569173fb..98fb565807 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt @@ -64,7 +64,6 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.core.content.ContextCompat.startActivity import androidx.hilt.navigation.compose.hiltViewModel -import com.firebase.ui.auth.AuthUI import io.getstream.video.android.BuildConfig import io.getstream.video.android.R import io.getstream.video.android.compose.theme.VideoTheme @@ -72,6 +71,8 @@ import io.getstream.video.android.ui.theme.Colors import io.getstream.video.android.ui.theme.LinkText import io.getstream.video.android.ui.theme.LinkTextData import io.getstream.video.android.ui.theme.StreamButton +import io.getstream.video.android.util.GoogleSignInHelper +import io.getstream.video.android.util.UserIdHelper @Composable fun LoginScreen( @@ -188,7 +189,7 @@ private fun LoginContent( text = "Login for Benchmark", onClick = { loginViewModel.handleUiEvent( - LoginEvent.SignInInSuccess("benchmark.test@getstream.io"), + LoginEvent.SignInSuccess("benchmark.test@getstream.io"), ) }, ) @@ -242,11 +243,8 @@ private fun EmailLoginDialog( .fillMaxWidth() .padding(horizontal = 16.dp), onClick = { - val userId = email - .replace(" ", "") - .replace(".", "") - .replace("@", "") - loginViewModel.handleUiEvent(LoginEvent.SignInInSuccess(userId)) + val userId = UserIdHelper.getUserIdFromEmail(email) + loginViewModel.handleUiEvent(LoginEvent.SignInSuccess(userId)) }, text = "Log in", ) @@ -263,34 +261,27 @@ private fun HandleLoginUiStates( loginViewModel: LoginViewModel = hiltViewModel(), ) { val context = LocalContext.current - val signInLauncher = rememberRegisterForActivityResult( + val signInLauncher = rememberLauncherForGoogleSignInActivityResult( onSignInSuccess = { email -> - val userId = email - .replace(" ", "") - .replace(".", "") - .replace("@", "") - loginViewModel.handleUiEvent(LoginEvent.SignInInSuccess(userId = userId)) + val userId = UserIdHelper.getUserIdFromEmail(email) + loginViewModel.handleUiEvent(LoginEvent.SignInSuccess(userId = userId)) }, onSignInFailed = { + loginViewModel.handleUiEvent(LoginEvent.Nothing) Toast.makeText(context, "Verification failed!", Toast.LENGTH_SHORT).show() }, ) LaunchedEffect(key1 = Unit) { - loginViewModel.sigInInIfValidUserExist() + loginViewModel.signInIfValidUserExist() } LaunchedEffect(key1 = loginUiState) { when (loginUiState) { is LoginUiState.GoogleSignIn -> { - val providers = arrayListOf( - AuthUI.IdpConfig.GoogleBuilder().build(), + signInLauncher.launch( + GoogleSignInHelper.getGoogleSignInClient(context).signInIntent, ) - - val signInIntent = AuthUI.getInstance().createSignInIntentBuilder() - .setAvailableProviders(providers) - .build() - signInLauncher.launch(signInIntent) } is LoginUiState.SignInComplete -> { diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt index 52c50ef236..1f137d2f56 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt @@ -18,18 +18,18 @@ package io.getstream.video.android.ui.login import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.firebase.auth.FirebaseAuth import dagger.hilt.android.lifecycle.HiltViewModel import io.getstream.log.streamLog import io.getstream.video.android.API_KEY import io.getstream.video.android.BuildConfig import io.getstream.video.android.core.StreamVideo +import io.getstream.video.android.data.repositories.GoogleAccountRepository import io.getstream.video.android.datastore.delegate.StreamUserDataStore import io.getstream.video.android.model.User import io.getstream.video.android.token.StreamVideoNetwork import io.getstream.video.android.token.TokenResponse import io.getstream.video.android.util.StreamVideoInitHelper -import io.getstream.video.android.util.UserIdGenerator +import io.getstream.video.android.util.UserIdHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow @@ -48,6 +48,7 @@ import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( private val dataStore: StreamUserDataStore, + private val googleAccountRepository: GoogleAccountRepository, ) : ViewModel() { private val event: MutableSharedFlow = MutableSharedFlow() @@ -56,7 +57,7 @@ class LoginViewModel @Inject constructor( when (event) { is LoginEvent.Loading -> flowOf(LoginUiState.Loading) is LoginEvent.GoogleSignIn -> flowOf(LoginUiState.GoogleSignIn) - is LoginEvent.SignInInSuccess -> signInInSuccess(event.userId) + is LoginEvent.SignInSuccess -> signInSuccess(event.userId) else -> flowOf(LoginUiState.Nothing) } }.shareIn(viewModelScope, SharingStarted.Lazily, 0) @@ -65,36 +66,34 @@ class LoginViewModel @Inject constructor( viewModelScope.launch { this@LoginViewModel.event.emit(event) } } - private fun signInInSuccess(email: String) = flow { + private fun signInSuccess(email: String) = flow { // skip login if we are already logged in (use has navigated back) if (StreamVideo.isInstalled) { emit(LoginUiState.AlreadyLoggedIn) } else { try { - val response = StreamVideoNetwork.tokenService.fetchToken( + val tokenResponse = StreamVideoNetwork.tokenService.fetchToken( userId = email, apiKey = API_KEY, ) - // if we are logged in with Google account then read the data (demo app doesn't have - // firebase login) - val authFirebaseUser = FirebaseAuth.getInstance().currentUser + val loggedInUser = googleAccountRepository.getCurrentUser() val user = User( - id = response.userId, - name = authFirebaseUser?.displayName ?: "", - image = authFirebaseUser?.photoUrl?.toString() ?: "", + id = tokenResponse.userId, + name = loggedInUser.name ?: "", + image = loggedInUser.photoUrl ?: "", role = "admin", - custom = mapOf("email" to response.userId), + custom = mapOf("email" to tokenResponse.userId), ) // Store the data in the demo app dataStore.updateUser(user) - dataStore.updateUserToken(response.token) + dataStore.updateUserToken(tokenResponse.token) // Init the Video SDK with the data StreamVideoInitHelper.loadSdk(dataStore) - emit(LoginUiState.SignInComplete(response)) + emit(LoginUiState.SignInComplete(tokenResponse)) } catch (exception: Throwable) { emit(LoginUiState.SignInFailure(exception.message ?: "General error")) streamLog { "Failed to fetch token - cause: $exception" } @@ -103,17 +102,17 @@ class LoginViewModel @Inject constructor( }.flowOn(Dispatchers.IO) init { - sigInInIfValidUserExist() + signInIfValidUserExist() } - fun sigInInIfValidUserExist() { + fun signInIfValidUserExist() { viewModelScope.launch { val user = dataStore.user.firstOrNull() if (user != null) { handleUiEvent(LoginEvent.Loading) if (!BuildConfig.BENCHMARK.toBoolean()) { delay(10) - handleUiEvent(LoginEvent.SignInInSuccess(userId = user.id)) + handleUiEvent(LoginEvent.SignInSuccess(userId = user.id)) } } else { // Production apps have an automatic guest login. Logging the user out @@ -121,8 +120,8 @@ class LoginViewModel @Inject constructor( if (BuildConfig.FLAVOR == "production") { handleUiEvent(LoginEvent.Loading) handleUiEvent( - LoginEvent.SignInInSuccess( - UserIdGenerator.generateRandomString(upperCaseOnly = true), + LoginEvent.SignInSuccess( + UserIdHelper.generateRandomString(upperCaseOnly = true), ), ) } @@ -152,5 +151,5 @@ sealed interface LoginEvent { data class GoogleSignIn(val id: String = UUID.randomUUID().toString()) : LoginEvent - data class SignInInSuccess(val userId: String) : LoginEvent + data class SignInSuccess(val userId: String) : LoginEvent } diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/outgoing/DebugCallScreen.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/outgoing/DebugCallScreen.kt deleted file mode 100644 index 5e405ac27b..0000000000 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/outgoing/DebugCallScreen.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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. - */ - -package io.getstream.video.android.ui.outgoing - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.border -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.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.material.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.mock.StreamMockUtils -import io.getstream.video.android.ui.theme.Colors -import io.getstream.video.android.ui.theme.StreamButton - -@Composable -fun DebugCallScreen( - navigateToRingCall: (callId: String, membersList: String) -> Unit, -) { - var callId by remember { mutableStateOf("") } - var membersList by remember { mutableStateOf("") } - - VideoTheme { - Box(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .fillMaxSize() - .background(Colors.background) - .padding(12.dp), - horizontalAlignment = Alignment.Start, - ) { - Text( - modifier = Modifier - .fillMaxWidth(), - text = "Call ID (optional)", - color = Color(0xFF979797), - fontSize = 13.sp, - ) - - Spacer(modifier = Modifier.height(4.dp)) - - TextField( - modifier = Modifier.border( - BorderStroke(1.dp, Color(0xFF4C525C)), - RoundedCornerShape(6.dp), - ), - value = callId, - onValueChange = { callId = it }, - colors = TextFieldDefaults.textFieldColors( - textColor = Color.White, - focusedLabelColor = VideoTheme.colors.primaryAccent, - unfocusedIndicatorColor = Colors.secondBackground, - focusedIndicatorColor = Colors.secondBackground, - backgroundColor = Colors.secondBackground, - ), - ) - - Spacer(modifier = Modifier.height(12.dp)) - - Text( - modifier = Modifier - .fillMaxWidth(), - text = "Members list - separated by comma", - color = Color(0xFF979797), - fontSize = 13.sp, - ) - - Spacer(modifier = Modifier.height(4.dp)) - - TextField( - modifier = Modifier.border( - BorderStroke(1.dp, Color(0xFF4C525C)), - RoundedCornerShape(6.dp), - ), - value = membersList, - onValueChange = { membersList = it }, - colors = TextFieldDefaults.textFieldColors( - textColor = Color.White, - focusedLabelColor = VideoTheme.colors.primaryAccent, - unfocusedIndicatorColor = Colors.secondBackground, - focusedIndicatorColor = Colors.secondBackground, - backgroundColor = Colors.secondBackground, - ), - ) - - Spacer(modifier = Modifier.height(4.dp)) - - StreamButton( - modifier = Modifier, - onClick = { - navigateToRingCall.invoke(callId, membersList) - }, - text = "Ring", - ) - } - } - } -} - -@Preview -@Composable -private fun DebugCallScreenPreview() { - StreamMockUtils.initializeStreamVideo(LocalContext.current) - VideoTheme { - DebugCallScreen( - navigateToRingCall = { _, _ -> }, - ) - } -} diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt new file mode 100644 index 0000000000..c903087172 --- /dev/null +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt @@ -0,0 +1,281 @@ +/* + * 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. + */ + +package io.getstream.video.android.ui.outgoing + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.RadioButton +import androidx.compose.material.RadioButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage +import coil.request.ImageRequest +import coil.size.Size +import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.ui.components.avatar.UserAvatar +import io.getstream.video.android.core.R +import io.getstream.video.android.mock.StreamMockUtils +import io.getstream.video.android.model.User +import io.getstream.video.android.ui.theme.Colors +import io.getstream.video.android.ui.theme.StreamImageButton + +@Composable +fun DirectCallJoinScreen( + viewModel: DirectCallViewModel = hiltViewModel(), + navigateToDirectCall: (memberList: String) -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = Unit) { viewModel.getGoogleAccounts() } + + VideoTheme { + Column( + modifier = Modifier + .fillMaxSize() + .background(Colors.background), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Header(user = uiState.currentUser) + + Body( + uiState = uiState, + toggleUserSelection = { viewModel.toggleGoogleAccountSelection(it) }, + onStartCallClick = navigateToDirectCall, + ) + } + } +} + +@Composable +private fun Header(user: User?) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp) // Outer padding + .padding(vertical = 12.dp), // Inner padding + verticalAlignment = Alignment.CenterVertically, + ) { + user?.let { + UserAvatar( + modifier = Modifier.size(24.dp), + userName = it.userNameOrId, + userImage = it.image, + ) + Spacer(modifier = Modifier.width(8.dp)) + } + + Text( + modifier = Modifier.weight(1f), + color = Color.White, + text = user?.name?.ifBlank { user.id }?.ifBlank { user.custom["email"] }.orEmpty(), + maxLines = 1, + fontSize = 16.sp, + ) + + Text( + text = stringResource(io.getstream.video.android.R.string.select_direct_call_users), + color = Color(0xFF979797), + fontSize = 13.sp, + ) + } +} + +@Composable +private fun Body( + uiState: DirectCallUiState, + toggleUserSelection: (Int) -> Unit, + onStartCallClick: (membersList: String) -> Unit, +) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 12.dp), + ) { + if (uiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .size(50.dp) + .align(Alignment.Center), + color = VideoTheme.colors.primaryAccent, + ) + } else { + uiState.googleAccounts?.let { users -> + UserList( + entries = users, + onUserClick = { clickedIndex -> toggleUserSelection(clickedIndex) }, + ) + StreamImageButton( // Floating button + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 10.dp), + enabled = users.any { it.isSelected }, + imageRes = R.drawable.stream_video_ic_call, + onClick = { + onStartCallClick( + users + .filter { it.isSelected } + .joinToString(separator = ",") { it.account.id ?: "" }, + ) + }, + ) + } ?: Text( + text = stringResource(io.getstream.video.android.R.string.cannot_load_google_account_list), + modifier = Modifier.align(Alignment.Center).padding(horizontal = 24.dp), + color = Color.White, + fontSize = 16.sp, + textAlign = TextAlign.Center, + lineHeight = 24.sp, + ) + } + } +} + +@Composable +private fun UserList(entries: List, onUserClick: (Int) -> Unit) { + Column(Modifier.verticalScroll(rememberScrollState())) { + entries.forEachIndexed { index, entry -> + UserRow( + index = index, + name = entry.account.name ?: "", + avatarUrl = entry.account.photoUrl, + isSelected = entry.isSelected, + onClick = { onUserClick(index) }, + ) + Spacer(modifier = Modifier.height(10.dp)) + } + } +} + +@Composable +private fun UserRow( + index: Int, + name: String, + avatarUrl: String?, + isSelected: Boolean, + onClick: (Int) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onClick(index) }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + UserAvatar(avatarUrl) + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = name, + color = Color.White, + fontSize = 16.sp, + ) + } + RadioButton( + selected = isSelected, + modifier = Modifier.size(20.dp), + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = VideoTheme.colors.primaryAccent, + unselectedColor = Color.LightGray, + ), + ) + } +} + +@Composable +private fun UserAvatar(url: String?) { + NetworkImage( + url = url ?: "", + modifier = Modifier + .size(50.dp) + .clip(shape = CircleShape), + crossfadeMillis = 200, + alpha = 0.8f, + error = ColorPainter(color = Color.DarkGray), + fallback = ColorPainter(color = Color.DarkGray), + placeholder = ColorPainter(color = Color.DarkGray), + ) +} + +@Composable +private fun NetworkImage( + url: String, + modifier: Modifier = Modifier, + crossfadeMillis: Int = 0, + alpha: Float = 1f, + error: Painter? = null, + fallback: Painter? = null, + placeholder: Painter? = null, +) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(url) + .size(Size.ORIGINAL) + .crossfade(durationMillis = crossfadeMillis) + .build(), + contentDescription = null, + modifier = modifier, + contentScale = ContentScale.Crop, + alpha = alpha, + error = error, + fallback = fallback, + placeholder = placeholder, + ) +} + +@Preview +@Composable +private fun DebugCallScreenPreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + DirectCallJoinScreen( + navigateToDirectCall = {}, + ) + } +} diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallViewModel.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallViewModel.kt new file mode 100644 index 0000000000..a665a9be76 --- /dev/null +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallViewModel.kt @@ -0,0 +1,92 @@ +/* + * 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. + */ + +package io.getstream.video.android.ui.outgoing + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import io.getstream.video.android.data.repositories.GoogleAccountRepository +import io.getstream.video.android.datastore.delegate.StreamUserDataStore +import io.getstream.video.android.model.User +import io.getstream.video.android.models.GoogleAccount +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DirectCallViewModel @Inject constructor( + private val userDataStore: StreamUserDataStore, + private val googleAccountRepository: GoogleAccountRepository, +) : ViewModel() { + private val _uiState = MutableStateFlow(DirectCallUiState()) + val uiState = _uiState.asStateFlow() + + init { + viewModelScope.launch { + _uiState.update { it.copy(currentUser = userDataStore.user.firstOrNull()) } + } + } + + fun getGoogleAccounts() { + _uiState.update { it.copy(isLoading = true) } + + viewModelScope.launch { + _uiState.update { + it.copy( + isLoading = false, + googleAccounts = googleAccountRepository.getAllAccounts()?.map { user -> + GoogleAccountUiState( + isSelected = false, + account = user, + ) + }, + ) + } + } + } + + fun toggleGoogleAccountSelection(selectedIndex: Int) { + _uiState.update { + it.copy( + googleAccounts = it.googleAccounts?.mapIndexed { index, accountUiState -> + if (index == selectedIndex) { + GoogleAccountUiState( + isSelected = !accountUiState.isSelected, + account = accountUiState.account, + ) + } else { + accountUiState + } + }, + ) + } + } +} + +data class DirectCallUiState( + val isLoading: Boolean = false, + val currentUser: User? = null, + val googleAccounts: List? = emptyList(), +) + +data class GoogleAccountUiState( + val isSelected: Boolean = false, + val account: GoogleAccount, +) diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/theme/StreamImageButton.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/theme/StreamImageButton.kt new file mode 100644 index 0000000000..929ffad991 --- /dev/null +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/theme/StreamImageButton.kt @@ -0,0 +1,53 @@ +/* + * 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. + */ + +package io.getstream.video.android.ui.theme + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import io.getstream.video.android.compose.theme.VideoTheme + +@Composable +fun StreamImageButton( + modifier: Modifier, + enabled: Boolean = true, + @DrawableRes imageRes: Int, + onClick: () -> Unit, +) { + Button( + modifier = modifier, + enabled = enabled, + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + backgroundColor = VideoTheme.colors.primaryAccent, + contentColor = VideoTheme.colors.primaryAccent, + disabledBackgroundColor = Colors.description, + disabledContentColor = Colors.description, + ), + onClick = onClick, + ) { + Image( + painter = painterResource(id = imageRes), + contentDescription = null, + ) + } +} diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/util/GoogleSignInHelper.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/util/GoogleSignInHelper.kt new file mode 100644 index 0000000000..0a7399064b --- /dev/null +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/util/GoogleSignInHelper.kt @@ -0,0 +1,36 @@ +/* + * 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. + */ + +package io.getstream.video.android.util + +import android.content.Context +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInClient +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.Scope +import io.getstream.video.android.R + +object GoogleSignInHelper { + fun getGoogleSignInClient(context: Context): GoogleSignInClient { + val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestEmail() + .requestIdToken(context.getString(R.string.default_web_client_id)) + .requestScopes(Scope("https://www.googleapis.com/auth/directory.readonly")) + .build() + + return GoogleSignIn.getClient(context, gso) + } +} diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt index ceb1382c76..06ddfa2d40 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt @@ -74,7 +74,7 @@ object StreamVideoInitHelper { // Create and login a random new user if user is null and we allow a random user login if (loggedInUser == null && useRandomUserAsFallback) { - val userId = UserIdGenerator.generateRandomString() + val userId = UserIdHelper.generateRandomString() val result = StreamVideoNetwork.tokenService.fetchToken( userId = userId, diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/util/UserIdGenerator.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/util/UserIdHelper.kt similarity index 87% rename from dogfooding/src/main/kotlin/io/getstream/video/android/util/UserIdGenerator.kt rename to dogfooding/src/main/kotlin/io/getstream/video/android/util/UserIdHelper.kt index 4b01c28564..435a4bf749 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/util/UserIdGenerator.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/util/UserIdHelper.kt @@ -16,8 +16,7 @@ package io.getstream.video.android.util -object UserIdGenerator { - +object UserIdHelper { fun generateRandomString(length: Int = 8, upperCaseOnly: Boolean = false): String { val allowedChars: List = ('A'..'Z') + ('0'..'9') + if (!upperCaseOnly) { ('a'..'z') @@ -29,4 +28,9 @@ object UserIdGenerator { .map { allowedChars.random() } .joinToString("") } + + fun getUserIdFromEmail(email: String) = email + .replace(" ", "") + .replace(".", "") + .replace("@", "") } diff --git a/dogfooding/src/main/res/values/strings.xml b/dogfooding/src/main/res/values/strings.xml index 0565c562f7..7e2dcf8206 100644 --- a/dogfooding/src/main/res/values/strings.xml +++ b/dogfooding/src/main/res/values/strings.xml @@ -35,6 +35,9 @@ Call ID Number Stream Video Try out a video call in this demo powered by Stream\'s video SDK + Cannot load user list.\nPlease try again or re-login into the app. + Direct Call + Select users and tap the call button below %s is typing %s and %d more are typing diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 407871d0f0..67e636fab9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -109,6 +109,7 @@ androidx-compose-tracing = { group = "androidx.compose.runtime", name = "runtime compose-stable-marker = { group = "com.github.skydoves", name = "compose-stable-marker", version.ref = "composeStableMarker" } coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } desugar = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar" } landscapist-coil = { group = "com.github.skydoves", name = "landscapist-coil", version.ref = "landscapist" } From 1493630153f77ac0bcbdb94b34f5df6e02ea221f Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Thu, 26 Oct 2023 14:07:33 +0900 Subject: [PATCH 4/8] Support dark themes for the reaction dialog (#889) * Support dark themes for the reaction dialog * Apply spotless --- .../video/android/ui/call/ReactionsMenu.kt | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/ReactionsMenu.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/ReactionsMenu.kt index 13ee6147b1..08922c75e2 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/ReactionsMenu.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/ReactionsMenu.kt @@ -18,6 +18,7 @@ package io.getstream.video.android.ui.call +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -35,7 +36,6 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -91,7 +91,7 @@ internal fun ReactionsMenu( val scope = rememberCoroutineScope() val modifier = Modifier .background( - color = Color.White, + color = VideoTheme.colors.barsBackground, shape = RoundedCornerShape(2.dp), ) .wrapContentWidth() @@ -102,13 +102,14 @@ internal fun ReactionsMenu( Dialog(onDismiss) { Card( modifier = modifier.wrapContentWidth(), + backgroundColor = VideoTheme.colors.barsBackground, ) { Column(Modifier.padding(16.dp)) { Row(horizontalArrangement = Arrangement.Center) { ReactionItem( modifier = Modifier .background( - color = Color(0xFFF1F4F0), + color = VideoTheme.colors.appBackground, shape = RoundedCornerShape(2.dp), ) .fillMaxWidth(), @@ -157,6 +158,7 @@ private fun ReactionItem( textAlign = TextAlign.Center, modifier = textModifier.padding(12.dp), text = "$mappedEmoji ${reaction.displayText}", + color = VideoTheme.colors.textHighEmphasis, ) } } @@ -168,20 +170,26 @@ private fun sendReaction(scope: CoroutineScope, call: Call, emoji: String, onDis } } -@Preview +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun ReactionItemPreview() { StreamMockUtils.initializeStreamVideo(LocalContext.current) - ReactionItem( - reactionMapper = ReactionMapper.defaultReactionMapper(), - onEmojiSelected = { - // Ignore - }, - reaction = DefaultReactionsMenuData.mainReaction, - ) + VideoTheme { + Box(modifier = Modifier.background(VideoTheme.colors.appBackground)) { + ReactionItem( + reactionMapper = ReactionMapper.defaultReactionMapper(), + onEmojiSelected = { + // Ignore + }, + reaction = DefaultReactionsMenuData.mainReaction, + ) + } + } } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun ReactionMenuPreview() { VideoTheme { @@ -189,7 +197,7 @@ private fun ReactionMenuPreview() { ReactionsMenu( call = mockCall, reactionMapper = ReactionMapper.defaultReactionMapper(), - onDismiss = { /* Do nothing */ }, + onDismiss = { }, ) } } From f26ccf4e08bd095da199fd64fdd40866803bdd4f Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Thu, 26 Oct 2023 15:05:15 +0900 Subject: [PATCH 5/8] Add customization properties to ReactionAction (#890) --- .../api/stream-video-android-compose.api | 2 +- .../ui/components/call/controls/actions/ReactionAction.kt | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) 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 4d8cc319e0..d09cdb1cab 100644 --- a/stream-video-android-compose/api/stream-video-android-compose.api +++ b/stream-video-android-compose/api/stream-video-android-compose.api @@ -747,7 +747,7 @@ public final class io/getstream/video/android/compose/ui/components/call/control } public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ReactionActionKt { - public static final fun ReactionAction (Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun ReactionAction-jB83MbM (Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/graphics/Shape;JJLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/video/android/compose/ui/components/call/controls/actions/RegularControlActionsKt { diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ReactionAction.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ReactionAction.kt index 3135d4218f..9b3f399f26 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ReactionAction.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ReactionAction.kt @@ -22,6 +22,7 @@ import androidx.compose.material.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -41,12 +42,17 @@ import io.getstream.video.android.ui.common.R public fun ReactionAction( modifier: Modifier = Modifier, enabled: Boolean = true, + shape: Shape = VideoTheme.shapes.callControlsButton, + enabledColor: Color = VideoTheme.colors.callActionIconEnabledBackground, + disabledColor: Color = VideoTheme.colors.callActionIconDisabledBackground, onCallAction: (Reaction) -> Unit, ) { CallControlActionBackground( modifier = modifier, isEnabled = true, - enabledColor = VideoTheme.colors.errorAccent, + shape = shape, + enabledColor = enabledColor, + disabledColor = disabledColor, ) { Icon( modifier = Modifier From cdd61d0574d6f44ef47009a50cebe192418c74fd Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 30 Oct 2023 12:42:34 +0100 Subject: [PATCH 6/8] Update CODEOWNERS (#893) Update codeowners to reflect the current teams on Github --- .github/CODEOWNERS | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a12cf1c69a..5de2dbe089 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,13 +5,13 @@ # Common # Core -stream-video-android @GetStream/android-developers-core -stream-video-android-pushprovider-firebase @GetStream/android-developers-core +stream-video-android @GetStream/android-developers +stream-video-android-pushprovider-firebase @GetStream/android-developers # UI -app @GetStream/android-developers-ui -dogfooding @GetStream/android-developers-ui -stream-video-android-compose @GetStream/android-developers-ui +app @GetStream/android-developers +dogfooding @GetStream/android-developers +stream-video-android-compose @GetStream/android-developers ### Top level files and directories # Git and GitHub @@ -40,4 +40,4 @@ DokkaRoot.md @GetStream/android-developers docusaurus @GetStream/android-developers # Misc -.idea @GetStream/android-developers \ No newline at end of file +.idea @GetStream/android-developers From bd3ec59b89f3e364cd1d6123d1157cf88b2647b8 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 30 Oct 2023 14:00:45 +0100 Subject: [PATCH 7/8] Spotlight view (#892) Add spotlight feature. Minor design update --- .../05-participants/02-participants-grid.mdx | 45 +-- .../04-participants-spotlight.mdx | 70 ++++ .../Android/assets/spotlight_landscape.png | Bin 0 -> 193541 bytes .../Android/assets/spotlight_portrait.png | Bin 0 -> 57944 bytes .../video/android/ui/call/CallScreen.kt | 26 ++ .../video/android/ui/call/LayoutChooser.kt | 321 ++++++++++++++++++ .../api/stream-video-android-compose.api | 75 +++- .../components/call/activecall/CallContent.kt | 38 ++- .../call/renderer/ParticipantVideo.kt | 14 +- ...ticipantsGrid.kt => ParticipantsLayout.kt} | 65 +++- .../call/renderer/ParticipantsRegularGrid.kt | 7 +- .../call/renderer/ParticipantsSpotlight.kt | 90 +++++ .../call/renderer/VideoRendererStyle.kt | 36 +- .../call/renderer/internal/Common.kt | 137 ++++++++ .../internal/LandscapeVideoRenderer.kt | 7 +- .../internal/LazyColumnVideoRenderer.kt | 38 ++- .../renderer/internal/LazyRowVideoRenderer.kt | 61 ++-- .../internal/OrientationVideoRenderer.kt | 28 -- .../PortraitScreenSharingVideoRenderer.kt | 12 +- .../internal/PortraitVideoRenderer.kt | 7 +- .../internal/SpotlightVideorenderer.kt | 276 +++++++++++++++ .../ui/extensions/ModifierExtensions.kt | 58 ++++ .../api/stream-video-android-core.api | 7 + .../android/core/call/state/CallAction.kt | 5 + .../src/main/res/values/dimens.xml | 8 +- 25 files changed, 1296 insertions(+), 135 deletions(-) create mode 100644 docusaurus/docs/Android/04-ui-components/05-participants/04-participants-spotlight.mdx create mode 100644 docusaurus/docs/Android/assets/spotlight_landscape.png create mode 100644 docusaurus/docs/Android/assets/spotlight_portrait.png create mode 100644 dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt rename stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/{ParticipantsGrid.kt => ParticipantsLayout.kt} (64%) create mode 100644 stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsSpotlight.kt create mode 100644 stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/Common.kt create mode 100644 stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/SpotlightVideorenderer.kt diff --git a/docusaurus/docs/Android/04-ui-components/05-participants/02-participants-grid.mdx b/docusaurus/docs/Android/04-ui-components/05-participants/02-participants-grid.mdx index cf32ffd2f7..31367168e7 100644 --- a/docusaurus/docs/Android/04-ui-components/05-participants/02-participants-grid.mdx +++ b/docusaurus/docs/Android/04-ui-components/05-participants/02-participants-grid.mdx @@ -1,34 +1,37 @@ -# ParticipantsGrid +# ParticipantsLayout -The `ParticipantsGrid` component is one of our most versatile and complex UI components, designed to render a list of participants in a call. It handles different UI layouts based on the number of participants and different screen orientations. Additionally, it can also render screen sharing content when there is an active session. +The `ParticipantsLayout` component is one of our most versatile and complex UI components, designed to render a list of participants in a call. It handles different UI layouts based on the number of participants and different screen orientations. Additionally, it can also render screen sharing content when there is an active session. Before jumping into how to use the component and how to customize it, let's review what some of these features mean. -What you can do with the `ParticipantsGrid` are: +What you can do with the `ParticipantsLayout` are: -- Displays a grid list of the remote/local participants. -- Supports landscape configuration. -- Renders [Screensharing](../04-call/05-screen-share-content.mdx). +- Displays a list of the remote/local participants. +- There are two available layouts, Grid and [Spotlight](04-participants-spotlight.mdx) +- There is also a dynamic option where the layout will switch automatically based on any pinned participants. +- All the layout variants are supported in portrait and in landscape mode +- Renders [Screensharing](../04-call/05-screen-share-content.mdx) on demand, regardless of selected layout. ### Flexible Layout -The `ParticipantsGrid` changes the UI layout based on the number of participants. In calls with fewer than four people, the local participant video is rendered in a floating item, using the [FloatingParticipantVideo](03-floating-participant-video.mdx). In calls with four or more people, it's rendered with other participants in a grid. +The `ParticipantsLayout` changes the UI layout based on the number of participants. In calls with fewer than four people, the local participant video is rendered in a floating item, using the [FloatingParticipantVideo](03-floating-participant-video.mdx). In calls with six or more people, it's rendered with other participants in a grid. Additionally, the participants are rendered in the following way: -* **One participant**: Rendered as the only single item in the "grid", taking up the full component space. +* **One participant**: Rendered as the only single item in the layout, taking up the full component space. * **Two participants** (1 remote + local): The remote participant is rendered within the full component space while the local participant is a floating item. -* **Three participants** (2 remote + local): Remote participants are in a vertical split-screen, while the local participant is a floating item. -* **Four or more participants**: (4 or more remote): Participants are rendered as a grid of items, in a paginated way. Up to 6 participants per page, with the sorted participant. +* **Three to four participants** (2-3 remote + local): Remote participants are in a vertical split-screen, while the local participant is a floating item. +* **Five or more** (4 remote + local): Participants are rendered as a grid of items, in a paginated way. Up to 6 participants per page, with the sorted participant. Sorted participants gives you the list of participants sorted by: - * anyone who is pinned -* dominant speaker * if you are screensharing -* last speaking at + +If the participants are not visible on the screen they are also sorted by: +* is dominant speaker +* has video enabled +* has audio enabled * all other video participants by when they joined -* audio only participants by when they joined ### Orientation @@ -36,9 +39,9 @@ The component handles both Landscape and Portrait orientations by rendering diff Additionally, both of these orientations work for screen sharing and adjust the UI accordingly. -| Portrait ParticipantsGrid | Landscape ParticipantsGrid | +| Portrait ParticipantsLayout | Landscape ParticipantsLayout | | ------- | ------------------------------------------------------------ | -| ![Portrait ParticipantsGrid](../../assets/compose_single_participant.png) | ![Landscape ParticipantsGrid](../../assets/compose_call_landscape.png) | +| ![Portrait ParticipantsLayout](../../assets/compose_single_participant.png) | ![Landscape ParticipantsLayout](../../assets/compose_call_landscape.png) | ### Screen Sharing @@ -50,7 +53,7 @@ Users can then focus on the shared content more or choose to enter the full scre | ------- | ------------------------------------------------------------ | | ![Portrait Screensharing](../../assets/compose_screensharing.png) | ![Landscape Screensharing](../../assets/compose_screensharing_landscape.png) | -Now that you've learned a lot about the `ParticipantsGrid` internal works, let's see how to use the component to add it to your UI. +Now that you've learned a lot about the `ParticipantsLayout` internal works, let's see how to use the component to add it to your UI. ## Usage @@ -58,11 +61,11 @@ To use the component in your UI, once you have the required state, you can rende ```kotlin @Composable -public fun MyParticipantsGridScreen() { +public fun MyParticipantsLayoutScreen() { Scaffold( topBar = { /* Custom top bar */ }, ) { padding -> - ParticipantsGrid( + ParticipantsLayout( modifier = Modifier.fillMaxSize(), call = call ) @@ -107,7 +110,7 @@ In terms of UI customization, you can very easily customize each participant vid ```kotlin @Composable -public fun ParticipantsGrid( +public fun ParticipantsLayout( modifier: Modifier = Modifier.fillMaxSize(), call = call, style = RegularVideoRendererStyle( @@ -125,7 +128,7 @@ With these options, you have more than enough space to customize how the compone You can also custom the entire video renderer by implementing your own video renderer: ```kotlin -ParticipantsGrid( +ParticipantsLayout( call = call, modifier = Modifier.fillMaxSize(), videoRenderer = { modifier, call, participant, style -> diff --git a/docusaurus/docs/Android/04-ui-components/05-participants/04-participants-spotlight.mdx b/docusaurus/docs/Android/04-ui-components/05-participants/04-participants-spotlight.mdx new file mode 100644 index 0000000000..86a9b7cb9a --- /dev/null +++ b/docusaurus/docs/Android/04-ui-components/05-participants/04-participants-spotlight.mdx @@ -0,0 +1,70 @@ +# ParticipantsSpotlight + +The `ParticipantsSpotlight` is a Composable component that allows you to highlight one participant and this one participant takes much of the screen, while the rest are rendered +either as a horizontal or vertical list depending on orientation. + +Let's see how to use the `ParticipantsSpotlight`. + +## Usage + +To use the `ParticipantsSpotlight` component in your app you can use it direcyly as a component or you can configure the [ParticipantsLayout](02-participants-grid.mdx) to display the spotlight. + +### Use it directly +```kotlin +ParticipantsSpotlight(call = call) +``` +The only mandatory parameter is `call` which represents the call for which the participants are being displayed. + +### Use it via [ParticipantsLayout](02-participants-grid.mdx) + +If you are using the `ParticipantsLayout` you can use an enum value `LayoutType` with one of three options. + +Those are: +```kotlin + //Automatically choose between Grid and Spotlight based on pinned participants and dominant speaker. + DYNAMIC + + //Force a spotlight view, showing the dominant speaker or the first speaker in the list. + SPOTLIGHT + + //Always show a grid layout, regardless of pinned participants. + GRID +``` + +Here is how it looks in action: +```kotlin +ParticipantsLayout( + layoutType = LayoutType.SPOTLIGHT, + call = call +) +``` + +The [ParticipantsLayout](02-participants-grid.mdx) internally displays the `ParticipantSpotlight` in two cases. +1. You have set the `layoutType` to `LayoutType.SPOTLIGHT` in which case a participant is always spotlighted. The participant shown in the spotlight is chosen based on the following order: + 1. is pinned + 2. is dominantSpeaker + 3. is first in the participants list +2. You have set the `LayoutType` to `LayoutType.DYNAMIC` in which case if there is a "pinned" participant, the spotlight view will be chosen in favor of grid. + +*Note*: `ParticipantLayout` will always prioritize screensharing regardless of the `LayoutType` if there is a [screensharing session](../04-call/05-screen-share-content.mdx).s + + +Using this component, you'll likely see something similar to the following UI: + +![Spotlight portrait](../../assets/spotlight_portrait.png) + +![Spotlight landscape](../../assets/spotlight_landscape.png) + + +Let's see how to customize this component. + +## Customization + +This is a very simple component so it doesn't have replaceable slots, but it still offers ways to customize its appearance. + +- `modifier`: Modifier for styling. +- `isZoomable`: Decide if this spotlight video renderer is zoomable or not. +- `style`: Defined properties for styling a single video call track. +- `videoRenderer`: A single video renderer renders each individual participant. + +If you're looking for guides on how to override and customize this UI, we have various [UI Cookbook](../../05-ui-cookbook/01-overview.mdx) recipes for you and we cover a portion of customization within the [Video Android SDK Tutorial](../../02-tutorials/01-video-calling.mdx). \ No newline at end of file diff --git a/docusaurus/docs/Android/assets/spotlight_landscape.png b/docusaurus/docs/Android/assets/spotlight_landscape.png new file mode 100644 index 0000000000000000000000000000000000000000..b1a05068a54b2916c1c7201259c1befe4c63f7e2 GIT binary patch literal 193541 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%0Aaiz}zBvsuzo=5?yK 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 Z^Ti2{2?ld=H?TSf);79UeD&_*{{p!k6VCtu literal 0 HcmV?d00001 diff --git a/docusaurus/docs/Android/assets/spotlight_portrait.png b/docusaurus/docs/Android/assets/spotlight_portrait.png new file mode 100644 index 0000000000000000000000000000000000000000..24bd5229c1c4eb6671abaf7687c8f02d36b1464c GIT binary patch literal 57944 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 LS3j3^P6 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 d09cdb1cab..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 { @@ -824,8 +826,8 @@ public final class io/getstream/video/android/compose/ui/components/call/rendere public final fun getLambda-4$stream_video_android_compose_release ()Lkotlin/jvm/functions/Function2; } -public final class io/getstream/video/android/compose/ui/components/call/renderer/ComposableSingletons$ParticipantsGridKt { - public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/renderer/ComposableSingletons$ParticipantsGridKt; +public final class io/getstream/video/android/compose/ui/components/call/renderer/ComposableSingletons$ParticipantsLayoutKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/renderer/ComposableSingletons$ParticipantsLayoutKt; public static field lambda-1 Lkotlin/jvm/functions/Function6; public static field lambda-2 Lkotlin/jvm/functions/Function2; public fun ()V @@ -849,10 +851,26 @@ public final class io/getstream/video/android/compose/ui/components/call/rendere public final fun getLambda-1$stream_video_android_compose_release ()Lkotlin/jvm/functions/Function6; } +public final class io/getstream/video/android/compose/ui/components/call/renderer/ComposableSingletons$ParticipantsSpotlightKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/renderer/ComposableSingletons$ParticipantsSpotlightKt; + public static field lambda-1 Lkotlin/jvm/functions/Function6; + public fun ()V + public final fun getLambda-1$stream_video_android_compose_release ()Lkotlin/jvm/functions/Function6; +} + public final class io/getstream/video/android/compose/ui/components/call/renderer/FloatingParticipantVideoKt { public static final fun FloatingParticipantVideo-f0nP0aY (Landroidx/compose/foundation/layout/BoxScope;Landroidx/compose/ui/Modifier;Lio/getstream/video/android/core/Call;Lio/getstream/video/android/core/ParticipantState;JLandroidx/compose/ui/Alignment;Lio/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V } +public final class io/getstream/video/android/compose/ui/components/call/renderer/LayoutType : java/lang/Enum { + public static final field DYNAMIC Lio/getstream/video/android/compose/ui/components/call/renderer/LayoutType; + public static final field GRID Lio/getstream/video/android/compose/ui/components/call/renderer/LayoutType; + public static final field SPOTLIGHT Lio/getstream/video/android/compose/ui/components/call/renderer/LayoutType; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/getstream/video/android/compose/ui/components/call/renderer/LayoutType; + public static fun values ()[Lio/getstream/video/android/compose/ui/components/call/renderer/LayoutType; +} + public final class io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideoKt { public static final fun ParticipantLabel (Landroidx/compose/foundation/layout/BoxScope;Lio/getstream/video/android/core/Call;Lio/getstream/video/android/core/ParticipantState;Landroidx/compose/ui/Alignment;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V public static final fun ParticipantLabel (Landroidx/compose/foundation/layout/BoxScope;Ljava/lang/String;Landroidx/compose/ui/Alignment;ZZFLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V @@ -860,8 +878,8 @@ public final class io/getstream/video/android/compose/ui/components/call/rendere public static final fun ParticipantVideoRenderer (Lio/getstream/video/android/core/Call;Lio/getstream/video/android/core/ParticipantState;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V } -public final class io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsGridKt { - public static final fun ParticipantsGrid (Lio/getstream/video/android/core/Call;Landroidx/compose/ui/Modifier;Lio/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle;Lkotlin/jvm/functions/Function6;Landroidx/compose/runtime/Composer;II)V +public final class io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsLayoutKt { + public static final fun ParticipantsLayout (Lio/getstream/video/android/core/Call;Landroidx/compose/ui/Modifier;Lio/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle;Lio/getstream/video/android/compose/ui/components/call/renderer/LayoutType;Lkotlin/jvm/functions/Function6;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsRegularGridKt { @@ -872,6 +890,10 @@ public final class io/getstream/video/android/compose/ui/components/call/rendere public static final fun ParticipantsScreenSharing (Lio/getstream/video/android/core/Call;Lio/getstream/video/android/core/model/ScreenSharingSession;Landroidx/compose/ui/Modifier;ZLio/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle;Lkotlin/jvm/functions/Function6;Landroidx/compose/runtime/Composer;II)V } +public final class io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsSpotlightKt { + public static final fun ParticipantsSpotlight (Lio/getstream/video/android/core/Call;Landroidx/compose/ui/Modifier;ZLio/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle;Lkotlin/jvm/functions/Function6;Landroidx/compose/runtime/Composer;II)V +} + public final class io/getstream/video/android/compose/ui/components/call/renderer/RegularVideoRendererStyle : io/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle { public static final field $stable I public fun ()V @@ -928,6 +950,34 @@ public final class io/getstream/video/android/compose/ui/components/call/rendere public fun toString ()Ljava/lang/String; } +public final class io/getstream/video/android/compose/ui/components/call/renderer/SpotlightVideoRendererStyle : io/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle { + public static final field $stable I + public fun ()V + public fun (ZZZZZLandroidx/compose/ui/Alignment;ILandroidx/compose/ui/Alignment;)V + public synthetic fun (ZZZZZLandroidx/compose/ui/Alignment;ILandroidx/compose/ui/Alignment;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Z + public final fun component2 ()Z + public final fun component3 ()Z + public final fun component4 ()Z + public final fun component5 ()Z + public final fun component6 ()Landroidx/compose/ui/Alignment; + public final fun component7 ()I + public final fun component8 ()Landroidx/compose/ui/Alignment; + public final fun copy (ZZZZZLandroidx/compose/ui/Alignment;ILandroidx/compose/ui/Alignment;)Lio/getstream/video/android/compose/ui/components/call/renderer/SpotlightVideoRendererStyle; + public static synthetic fun copy$default (Lio/getstream/video/android/compose/ui/components/call/renderer/SpotlightVideoRendererStyle;ZZZZZLandroidx/compose/ui/Alignment;ILandroidx/compose/ui/Alignment;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/call/renderer/SpotlightVideoRendererStyle; + public fun equals (Ljava/lang/Object;)Z + public fun getLabelPosition ()Landroidx/compose/ui/Alignment; + public fun getReactionDuration ()I + public fun getReactionPosition ()Landroidx/compose/ui/Alignment; + public fun hashCode ()I + public fun isFocused ()Z + public fun isScreenSharing ()Z + public fun isShowingConnectionQualityIndicator ()Z + public fun isShowingParticipantLabel ()Z + public fun isShowingReactions ()Z + public fun toString ()Ljava/lang/String; +} + public abstract class io/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle { public static final field $stable I public synthetic fun (ZZZZZLandroidx/compose/ui/Alignment;ILandroidx/compose/ui/Alignment;Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -1039,6 +1089,21 @@ public final class io/getstream/video/android/compose/ui/components/call/rendere public final fun getLambda-8$stream_video_android_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/video/android/compose/ui/components/call/renderer/internal/ComposableSingletons$SpotlightVideorendererKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/renderer/internal/ComposableSingletons$SpotlightVideorendererKt; + public static field lambda-1 Lkotlin/jvm/functions/Function6; + public static field lambda-2 Lkotlin/jvm/functions/Function2; + public static field lambda-3 Lkotlin/jvm/functions/Function2; + public static field lambda-4 Lkotlin/jvm/functions/Function2; + public static field lambda-5 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/Function2; + public final fun getLambda-3$stream_video_android_compose_release ()Lkotlin/jvm/functions/Function2; + 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 class io/getstream/video/android/compose/ui/components/call/renderer/internal/ScreenShareVideoRendererKt { public static final fun ScreenShareVideoRenderer (Lio/getstream/video/android/core/Call;Lio/getstream/video/android/core/model/ScreenSharingSession;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;ZZLandroidx/compose/runtime/Composer;II)V } 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 e12cf38c59..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,16 +64,19 @@ 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.ParticipantsGrid +import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantsLayout import io.getstream.video.android.compose.ui.components.call.renderer.RegularVideoRendererStyle import io.getstream.video.android.compose.ui.components.call.renderer.VideoRendererStyle 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, ) }, @@ -118,7 +130,8 @@ public fun CallContent( ) }, videoContent: @Composable RowScope.(call: Call) -> Unit = { - ParticipantsGrid( + 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/ParticipantVideo.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt index bf2dd9fb37..5d27fd274a 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt @@ -52,7 +52,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -294,6 +296,10 @@ public fun BoxScope.ParticipantLabel( ) }, ) { + var componentWidth by remember { mutableStateOf(0.dp) } + componentWidth = VideoTheme.dimens.participantLabelTextMaxWidth + // get local density from composable + val density = LocalDensity.current Box( modifier = Modifier .align(labelPosition) @@ -302,7 +308,11 @@ public fun BoxScope.ParticipantLabel( .background( VideoTheme.colors.participantLabelBackground, shape = VideoTheme.shapes.participantLabelShape, - ), + ).onGloballyPositioned { + componentWidth = with(density) { + it.size.width.toDp() + } + }, ) { Row( modifier = Modifier.align(Center), @@ -310,7 +320,7 @@ public fun BoxScope.ParticipantLabel( ) { Text( modifier = Modifier - .widthIn(max = VideoTheme.dimens.participantLabelTextMaxWidth) + .widthIn(max = componentWidth) .padding(start = VideoTheme.dimens.participantLabelTextPaddingStart) .align(CenterVertically), text = nameLabel, diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsGrid.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsLayout.kt similarity index 64% rename from stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsGrid.kt rename to stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsLayout.kt index 2f2b65ea95..426efc9c2f 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsGrid.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsLayout.kt @@ -18,9 +18,11 @@ package io.getstream.video.android.compose.ui.components.call.renderer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.getstream.video.android.compose.theme.VideoTheme @@ -29,6 +31,17 @@ import io.getstream.video.android.core.ParticipantState import io.getstream.video.android.mock.StreamMockUtils import io.getstream.video.android.mock.mockCall +public enum class LayoutType { + /** Automatically choose between Grid and Spotlight based on pinned participants and dominant speaker. */ + DYNAMIC, + + /** Force a spotlight view, showing the dominant speaker or the first speaker in the list. */ + SPOTLIGHT, + + /** Always show a grid layout, regardless of pinned participants. */ + GRID, +} + /** * Renders all the participants, based on the number of people in a call and the call state. * Also takes into account if there are any screen sharing sessions active and adjusts the UI @@ -37,13 +50,15 @@ import io.getstream.video.android.mock.mockCall * @param call The call that contains all the participants state and tracks. * @param modifier Modifier for styling. * @param style Defined properties for styling a single video call track. + * @param layoutType The type of layout. [LayoutType], default - [LayoutType.DYNAMIC] * @param videoRenderer A single video renderer renders each individual participant. */ @Composable -public fun ParticipantsGrid( +public fun ParticipantsLayout( call: Call, modifier: Modifier = Modifier, style: VideoRendererStyle = RegularVideoRendererStyle(), + layoutType: LayoutType = LayoutType.DYNAMIC, videoRenderer: @Composable ( modifier: Modifier, call: Call, @@ -58,25 +73,38 @@ public fun ParticipantsGrid( ) }, ) { - if (LocalInspectionMode.current) { - ParticipantsRegularGrid( - call = call, - modifier = modifier, - ) - return - } - val screenSharingSession = call.state.screenSharingSession.collectAsStateWithLifecycle() val screenSharing = screenSharingSession.value + val pinnedParticipants by call.state.pinnedParticipants.collectAsStateWithLifecycle() + val showSpotlight by remember(key1 = pinnedParticipants, key2 = layoutType) { + derivedStateOf { + when (layoutType) { + LayoutType.GRID -> false + LayoutType.SPOTLIGHT -> true + else -> pinnedParticipants.isNotEmpty() + } + } + } - // We do not display our own screen-sharing session if (screenSharing == null || screenSharing.participant.isLocal) { - ParticipantsRegularGrid( - call = call, - modifier = modifier, - style = style, - videoRenderer = videoRenderer, - ) + if (showSpotlight) { + ParticipantsSpotlight( + call = call, + modifier = modifier, + style = SpotlightVideoRendererStyle().copy( + isFocused = style.isFocused, + isShowingReactions = style.isShowingReactions, + labelPosition = style.labelPosition, + ), + ) + } else { + ParticipantsRegularGrid( + call = call, + modifier = modifier, + style = style, + videoRenderer = videoRenderer, + ) + } } else { ParticipantsScreenSharing( call = call, @@ -97,9 +125,10 @@ public fun ParticipantsGrid( private fun CallVideoRendererPreview() { StreamMockUtils.initializeStreamVideo(LocalContext.current) VideoTheme { - ParticipantsGrid( + ParticipantsLayout( call = mockCall, modifier = Modifier.fillMaxWidth(), + layoutType = LayoutType.GRID, ) } } diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsRegularGrid.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsRegularGrid.kt index a2fb6da380..21a8064190 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsRegularGrid.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsRegularGrid.kt @@ -19,7 +19,6 @@ package io.getstream.video.android.compose.ui.components.call.renderer import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -67,11 +66,7 @@ public fun ParticipantsRegularGrid( ) { var parentSize: IntSize by remember { mutableStateOf(IntSize(0, 0)) } - Box( - modifier = modifier - .background(color = VideoTheme.colors.appBackground) - .padding(VideoTheme.dimens.participantsGridPadding), - ) { + Box(modifier = modifier.background(color = VideoTheme.colors.appBackground)) { val roomParticipants by call.state.participants.collectAsStateWithLifecycle() if (roomParticipants.isNotEmpty()) { diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsSpotlight.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsSpotlight.kt new file mode 100644 index 0000000000..1a49d51325 --- /dev/null +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsSpotlight.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2014-2022 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. + */ + +package io.getstream.video.android.compose.ui.components.call.renderer + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.getstream.video.android.compose.ui.components.call.renderer.internal.SpotlightVideoRenderer +import io.getstream.video.android.core.Call +import io.getstream.video.android.core.ParticipantState + +/** + * Renders all the CallParticipants, based on the number of people in a call and the call state. + * Also takes into account if there are any screen sharing sessions active and adjusts the UI + * accordingly. + * + * @param call The call that contains all the participants state and tracks. + * @param modifier Modifier for styling. + * @param isZoomable Decide to this screensharing video renderer is zoomable or not. + * @param style Defined properties for styling a single video call track. + * @param videoRenderer A single video renderer renders each individual participant. + */ +@Composable +public fun ParticipantsSpotlight( + call: Call, + modifier: Modifier = Modifier, + isZoomable: Boolean = false, + style: VideoRendererStyle = SpotlightVideoRendererStyle(), + videoRenderer: @Composable ( + modifier: Modifier, + call: Call, + participant: ParticipantState, + style: VideoRendererStyle, + ) -> Unit = { videoModifier, videoCall, videoParticipant, videoStyle -> + ParticipantVideo( + modifier = videoModifier, + call = videoCall, + participant = videoParticipant, + style = videoStyle, + ) + }, +) { + val configuration = LocalConfiguration.current + val participants by call.state.participants.collectAsStateWithLifecycle(emptyList()) + val dominantSpeaker by call.state.dominantSpeaker.collectAsStateWithLifecycle() + val pinnedParticipant by call.state.pinnedParticipants.collectAsStateWithLifecycle() + val speaker by remember(key1 = dominantSpeaker, key2 = pinnedParticipant, key3 = participants) { + derivedStateOf { + val pinnedSpeakerId = pinnedParticipant.keys.firstOrNull() + val pinnedSpeaker = participants.find { it.sessionId == pinnedSpeakerId } + pinnedSpeaker ?: dominantSpeaker ?: participants.firstOrNull() + } + } + + Box( + modifier = modifier.fillMaxSize(), + ) { + // Either the dominant speaker, or the first participant in the spotlight + SpotlightVideoRenderer( + call = call, + speaker = speaker, + participants = participants, + orientation = configuration.orientation, + modifier = modifier.fillMaxSize(), + isZoomable = isZoomable, + style = style, + videoRenderer = videoRenderer, + ) + } +} diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle.kt index 30041a95e5..d0f6897a27 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/VideoRendererStyle.kt @@ -145,7 +145,41 @@ public data class ScreenSharingVideoRendererStyle( override val isShowingConnectionQualityIndicator: Boolean = false, override val labelPosition: Alignment = Alignment.BottomStart, override val reactionDuration: Int = 1000, - override val reactionPosition: Alignment = Alignment.Center, + override val reactionPosition: Alignment = Alignment.TopEnd, + +) : VideoRendererStyle( + isFocused, + isScreenSharing, + isShowingReactions, + isShowingParticipantLabel, + isShowingConnectionQualityIndicator, + labelPosition, + reactionDuration, + reactionPosition, +) + +/** + * A spotlight video renderer style, which displays the reactions, and participant label. + * + * @param isFocused Represents whether the participant is focused or not. + * @param isScreenSharing Represents whether the video renderer is about screen sharing. + * @param isShowingReactions Represents whether display reaction comes from the call state. + * @param isShowingParticipantLabel Represents whether display the participant label that contains the name and microphone status of a participant. + * @param isShowingConnectionQualityIndicator Represents whether displays the connection quality indicator or not. + * @param labelPosition The position of the participant label that contains the name and microphone status of a participant. + * @param reactionDuration The duration of the reaction animation. + * @param reactionPosition The position of the reaction. + */ +@Stable +public data class SpotlightVideoRendererStyle( + override val isFocused: Boolean = false, + override val isScreenSharing: Boolean = false, + override val isShowingReactions: Boolean = true, + override val isShowingParticipantLabel: Boolean = true, + override val isShowingConnectionQualityIndicator: Boolean = true, + override val labelPosition: Alignment = Alignment.BottomStart, + override val reactionDuration: Int = 1000, + override val reactionPosition: Alignment = Alignment.TopEnd, ) : VideoRendererStyle( isFocused, diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/Common.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/Common.kt new file mode 100644 index 0000000000..90d5c48535 --- /dev/null +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/Common.kt @@ -0,0 +1,137 @@ +/* + * 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. + */ + +package io.getstream.video.android.compose.ui.components.call.renderer.internal + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.core.Call +import kotlinx.coroutines.flow.Flow + +/** + * Provides a [LazyGridState] or [LazyListState] depending on the original parameter supplied. + * The lazy state will update the [Call.state] with visibility information about the items. + * + * Creates a [snapshotFlow] of the visible items state and supplies it to the [Call.state] via + * [DisposableEffect] + * + * @param call the call. + * @param original the original lazy state. Either [LazyGridState] or [LazyListState] + * @return the original supplied state. + */ +@Composable +internal fun lazyStateWithVisibilityNotification(call: Call, original: T): T { + val snapshotFlow: Flow> = when (original) { + // This duplicate code must be here because while the names are the same, the types are different. + is LazyGridState -> { + snapshotFlow { + original.layoutInfo.visibleItemsInfo.map { + it.key as String + } + } + } + is LazyListState -> + snapshotFlow { + original.layoutInfo.visibleItemsInfo.map { + it.key as String + } + } + else -> throw UnsupportedOperationException("Wrong initial state.") // Currently + } + DisposableEffect(key1 = call, effect = { + call.state.updateParticipantVisibilityFlow(snapshotFlow) + + onDispose { + call.state.updateParticipantVisibilityFlow(null) + } + }) + return original +} + +/** + * Wraps a content that needs to be spotlighted at top of the screen. + * Used in [PortraitScreenSharingVideoRenderer] and [SpotlightVideoRenderer]. + * + * @param modifier the modifier + * @param background the background color if there is no content or content is loading + * @param content the content to be displayed. + */ +@Composable +internal fun SpotlightContentPortrait( + modifier: Modifier, + background: Color, + content: @Composable () -> Unit, +) { + Column( + modifier = modifier + .padding(VideoTheme.dimens.participantsGridPadding), + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(background) + .fillMaxWidth(), + ) { + content() + } + } +} + +/** + * Wraps a content that needs to be spotlighted at top of the screen. + * Used in [PortraitScreenSharingVideoRenderer] and [SpotlightVideoRenderer]. + * + * @param modifier the modifier + * @param background the background color if there is no content or content is loading + * @param content the content to be displayed. + */ +@Composable +internal fun SpotlightContentLandscape( + modifier: Modifier, + background: Color, + content: @Composable () -> Unit, +) { + Row( + modifier = modifier + .padding(VideoTheme.dimens.participantsGridPadding), + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(background) + .fillMaxSize(), + ) { + content() + } + } +} diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeVideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeVideoRenderer.kt index 2d4f24092b..0250d811b6 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeVideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeVideoRenderer.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -149,7 +150,11 @@ internal fun BoxScope.LandscapeVideoRenderer( else -> { BoxWithConstraints(modifier = Modifier.fillMaxHeight()) { - val gridState = lazyGridStateWithVisibilityNotification(call = call) + val gridState = + lazyStateWithVisibilityNotification( + call = call, + original = rememberLazyGridState(), + ) LazyVerticalGrid( modifier = Modifier.fillMaxSize(), state = gridState, diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyColumnVideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyColumnVideoRenderer.kt index 479a323c0d..af02b98b13 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyColumnVideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyColumnVideoRenderer.kt @@ -17,15 +17,14 @@ package io.getstream.video.android.compose.ui.components.call.renderer.internal import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import io.getstream.video.android.compose.theme.VideoTheme @@ -33,6 +32,7 @@ import io.getstream.video.android.compose.ui.components.call.renderer.Participan import io.getstream.video.android.compose.ui.components.call.renderer.ScreenSharingVideoRendererStyle import io.getstream.video.android.compose.ui.components.call.renderer.VideoRendererStyle import io.getstream.video.android.compose.ui.components.call.renderer.copy +import io.getstream.video.android.compose.ui.extensions.topOrBottomPadding import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState import io.getstream.video.android.mock.StreamMockUtils @@ -49,6 +49,11 @@ import io.getstream.video.android.mock.mockParticipantList @Composable internal fun LazyColumnVideoRenderer( modifier: Modifier = Modifier, + state: LazyListState = rememberLazyListState(), + itemModifier: Modifier = Modifier.size( + VideoTheme.dimens.screenShareParticipantItemSize * 1.4f, + VideoTheme.dimens.screenShareParticipantItemSize, + ), call: Call, participants: List, dominantSpeaker: ParticipantState?, @@ -68,14 +73,24 @@ internal fun LazyColumnVideoRenderer( }, ) { LazyColumn( - modifier = modifier.padding(vertical = VideoTheme.dimens.screenShareParticipantsRowPadding), + modifier = modifier, + state = state, verticalArrangement = Arrangement.spacedBy( VideoTheme.dimens.screenShareParticipantsListItemMargin, ), horizontalAlignment = Alignment.CenterHorizontally, content = { - items(items = participants, key = { it.sessionId }) { participant -> + itemsIndexed( + items = participants, + key = { _, it -> it.sessionId }, + ) { index, participant -> ListVideoRenderer( + modifier = itemModifier.topOrBottomPadding( + value = VideoTheme.dimens.participantsGridPadding, + index = index, + first = 0, + last = participants.lastIndex, + ), call = call, participant = participant, dominantSpeaker = dominantSpeaker, @@ -97,6 +112,7 @@ internal fun LazyColumnVideoRenderer( private fun ListVideoRenderer( call: Call, participant: ParticipantState, + modifier: Modifier = Modifier, dominantSpeaker: ParticipantState?, style: VideoRendererStyle = ScreenSharingVideoRendererStyle(), videoRenderer: @Composable ( @@ -114,12 +130,10 @@ private fun ListVideoRenderer( }, ) { videoRenderer.invoke( - modifier = Modifier - .size(VideoTheme.dimens.screenShareParticipantItemSize) - .clip(RoundedCornerShape(VideoTheme.dimens.screenShareParticipantsRadius)), - call = call, - participant = participant, - style = style.copy( + modifier, + call, + participant, + style.copy( isFocused = participant.sessionId == dominantSpeaker?.sessionId, ), ) diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyRowVideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyRowVideoRenderer.kt index faf05ee32b..8287d70954 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyRowVideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyRowVideoRenderer.kt @@ -17,15 +17,14 @@ package io.getstream.video.android.compose.ui.components.call.renderer.internal import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import io.getstream.video.android.compose.theme.VideoTheme @@ -33,6 +32,7 @@ import io.getstream.video.android.compose.ui.components.call.renderer.Participan import io.getstream.video.android.compose.ui.components.call.renderer.ScreenSharingVideoRendererStyle import io.getstream.video.android.compose.ui.components.call.renderer.VideoRendererStyle import io.getstream.video.android.compose.ui.components.call.renderer.copy +import io.getstream.video.android.compose.ui.extensions.startOrEndPadding import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState import io.getstream.video.android.mock.StreamMockUtils @@ -45,13 +45,19 @@ import io.getstream.video.android.mock.mockParticipantList * @param call The state of the call. * @param participants List of participants to show. * @param modifier Modifier for styling. + * @param state [LazyListState] if needed from outside */ @Composable internal fun LazyRowVideoRenderer( modifier: Modifier = Modifier, + itemModifier: Modifier = Modifier.size( + VideoTheme.dimens.screenShareParticipantItemSize * 1.5f, + VideoTheme.dimens.screenShareParticipantItemSize, + ), call: Call, participants: List, dominantSpeaker: ParticipantState?, + state: LazyListState = rememberLazyListState(), style: VideoRendererStyle = ScreenSharingVideoRendererStyle(), videoRenderer: @Composable ( modifier: Modifier, @@ -68,25 +74,29 @@ internal fun LazyRowVideoRenderer( }, ) { LazyRow( - modifier = modifier.padding( - horizontal = VideoTheme.dimens.screenShareParticipantsRowPadding, - ), + state = state, + modifier = modifier, horizontalArrangement = Arrangement.spacedBy( VideoTheme.dimens.screenShareParticipantsListItemMargin, ), verticalAlignment = Alignment.CenterVertically, - content = { - items(items = participants, key = { it.sessionId }) { participant -> - ListVideoRenderer( - call = call, - participant = participant, - dominantSpeaker = dominantSpeaker, - style = style, - videoRenderer = videoRenderer, - ) - } - }, - ) + ) { + itemsIndexed(items = participants, key = { _, it -> it.sessionId }) { index, participant -> + ListVideoRenderer( + modifier = itemModifier.startOrEndPadding( + value = VideoTheme.dimens.participantsGridPadding, + index = index, + first = 0, + last = participants.lastIndex, + ), + call = call, + participant = participant, + dominantSpeaker = dominantSpeaker, + style = style, + videoRenderer = videoRenderer, + ) + } + } } /** @@ -97,6 +107,7 @@ internal fun LazyRowVideoRenderer( */ @Composable private fun ListVideoRenderer( + modifier: Modifier = Modifier, call: Call, participant: ParticipantState, dominantSpeaker: ParticipantState?, @@ -115,15 +126,11 @@ private fun ListVideoRenderer( ) }, ) { - val height = VideoTheme.dimens.screenShareParticipantItemSize - val width = height * 1.5f videoRenderer.invoke( - modifier = Modifier - .size(width, height) - .clip(RoundedCornerShape(VideoTheme.dimens.screenShareParticipantsRadius)), - call = call, - participant = participant, - style = style.copy( + modifier, + call, + participant, + style.copy( isFocused = participant.sessionId == dominantSpeaker?.sessionId, ), ) diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/OrientationVideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/OrientationVideoRenderer.kt index 3f65cec365..437c16ba73 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/OrientationVideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/OrientationVideoRenderer.kt @@ -18,14 +18,10 @@ package io.getstream.video.android.compose.ui.components.call.renderer.internal import android.content.res.Configuration.ORIENTATION_LANDSCAPE import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.lazy.grid.LazyGridState -import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.IntSize @@ -102,27 +98,3 @@ internal fun BoxScope.OrientationVideoRenderer( ) } } - -/** - * Creates a [LazyGridState] which also monitors the visibility of items on the UI and exposes - * a snapshot flow to the [Call]. - * - * @param call the current call. - */ -@Composable -internal fun lazyGridStateWithVisibilityNotification(call: Call): LazyGridState { - val gridState = rememberLazyGridState() - val snapshotFlow = snapshotFlow { - gridState.layoutInfo.visibleItemsInfo.map { - it.key as String - } - } - DisposableEffect(key1 = call, effect = { - call.state.updateParticipantVisibilityFlow(snapshotFlow) - - onDispose { - call.state.updateParticipantVisibilityFlow(null) - } - }) - return gridState -} diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt index 23c12243d4..0f48750253 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt @@ -101,12 +101,12 @@ internal fun PortraitScreenSharingVideoRenderer( content = { item(span = { GridItemSpan(2) }) { ScreenSharingContent( - modifier, - call, - session, - isZoomable, - me, - sharingParticipant, + modifier = modifier, + call = call, + session = session, + isZoomable = isZoomable, + me = me, + sharingParticipant = sharingParticipant, ) } items( diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt index bbaa376578..451ddb4d9d 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -149,7 +150,11 @@ internal fun BoxScope.PortraitVideoRenderer( } else -> { BoxWithConstraints(modifier = Modifier.fillMaxSize()) { - val gridState = lazyGridStateWithVisibilityNotification(call = call) + val gridState = + lazyStateWithVisibilityNotification( + call = call, + original = rememberLazyGridState(), + ) LazyVerticalGrid( modifier = Modifier.fillMaxSize(), columns = GridCells.Fixed(2), diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/SpotlightVideorenderer.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/SpotlightVideorenderer.kt new file mode 100644 index 0000000000..41c205d8c3 --- /dev/null +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/SpotlightVideorenderer.kt @@ -0,0 +1,276 @@ +/* + * 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. + */ + +package io.getstream.video.android.compose.ui.components.call.renderer.internal + +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.content.res.Configuration.ORIENTATION_PORTRAIT +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +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.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantVideo +import io.getstream.video.android.compose.ui.components.call.renderer.SpotlightVideoRendererStyle +import io.getstream.video.android.compose.ui.components.call.renderer.VideoRendererStyle +import io.getstream.video.android.core.Call +import io.getstream.video.android.core.ParticipantState +import io.getstream.video.android.mock.StreamMockUtils +import io.getstream.video.android.mock.mockCall +import io.getstream.video.android.mock.mockParticipant +import io.getstream.video.android.mock.mockParticipantList +import me.saket.telephoto.zoomable.rememberZoomableState +import me.saket.telephoto.zoomable.zoomable +import java.lang.Integer.max + +@Composable +internal fun SpotlightVideoRenderer( + modifier: Modifier = Modifier, + call: Call, + speaker: ParticipantState?, + participants: List, + orientation: Int = ORIENTATION_PORTRAIT, + isZoomable: Boolean = true, + style: VideoRendererStyle = SpotlightVideoRendererStyle(), + videoRenderer: @Composable ( + modifier: Modifier, + call: Call, + participant: ParticipantState, + style: VideoRendererStyle, + ) -> Unit = { videoModifier, videoCall, videoParticipant, videoStyle -> + ParticipantVideo( + modifier = videoModifier, + call = videoCall, + participant = videoParticipant, + style = videoStyle, + ) + }, +) { + if (participants.size == 1) { + // Just display the one participant + videoRenderer.invoke( + modifier.fillMaxSize().padding(VideoTheme.dimens.participantsGridPadding), + call, + participants[0], + style, + ) + return + } + + val derivedParticipants by remember(key1 = participants, key2 = speaker) { + derivedStateOf { + participants.filterNot { + it.sessionId == speaker?.sessionId + } + } + } + val listState = + lazyStateWithVisibilityNotification(call = call, original = rememberLazyListState()) + + Box(modifier = modifier.fillMaxSize()) { + if (ORIENTATION_LANDSCAPE == orientation) { + Row { + SpotlightContentLandscape( + modifier = modifier.weight(0.7f), + background = VideoTheme.colors.participantContainerBackground, + ) { + SpeakerSpotlight(speaker, videoRenderer, isZoomable, call, style) + } + LazyColumnVideoRenderer( + state = listState, + itemModifier = Modifier.fillHeightIfParticipantsCount(3, participants.size), + modifier = Modifier + .align(CenterVertically) + .wrapContentSize(), + call = call, + participants = derivedParticipants, + dominantSpeaker = speaker, + style = style, + videoRenderer = videoRenderer, + ) + } + } else { + // *2 to account for the controls + Column( + modifier = Modifier.padding(bottom = VideoTheme.dimens.participantsGridPadding * 2), + ) { + SpotlightContentPortrait( + modifier = modifier.weight(1f), + background = VideoTheme.colors.participantContainerBackground, + ) { + SpeakerSpotlight( + speaker = speaker, + videoRenderer = videoRenderer, + isZoomable = isZoomable, + call = call, + style = style, + ) + } + LazyRowVideoRenderer( + state = listState, + itemModifier = Modifier.fillWidthIfParticipantCount(3, participants.size), + modifier = Modifier + .wrapContentWidth() + .align(CenterHorizontally) + .height(VideoTheme.dimens.screenShareParticipantItemSize), + call = call, + participants = derivedParticipants, + dominantSpeaker = speaker, + style = style, + videoRenderer = videoRenderer, + ) + } + } + } +} + +@Composable +private fun SpeakerSpotlight( + speaker: ParticipantState?, + videoRenderer: @Composable ( + modifier: Modifier, + call: Call, + participant: ParticipantState, + style: VideoRendererStyle, + ) -> Unit, + isZoomable: Boolean, + call: Call, + style: VideoRendererStyle, +) { + if (speaker != null) { + videoRenderer.invoke( + Modifier + .fillMaxSize() + .zoomable(rememberZoomableState(), isZoomable), + call, + speaker, + style, + ) + } +} + +private fun Modifier.fillWidthIfParticipantCount(fillCount: Int, totalCount: Int): Modifier = composed { + // -1 because one user is in spotlight + val itemWidth = LocalConfiguration.current.screenWidthDp / max(fillCount - 1, 1) + when (totalCount) { + fillCount -> this.fillMaxHeight().width(itemWidth.dp) + else -> this.size( + VideoTheme.dimens.screenShareParticipantItemSize * 1.5f, + VideoTheme.dimens.screenShareParticipantItemSize, + ) + } +} +private fun Modifier.fillHeightIfParticipantsCount( + fillCount: Int, + totalCount: Int, +): Modifier = composed { + // -1 because one user is in spotlight + val itemHeight = LocalConfiguration.current.screenHeightDp / max(fillCount - 1, 1) + when (totalCount) { + fillCount -> this.size( + VideoTheme.dimens.screenShareParticipantItemSize * 1.5f, + itemHeight.dp, + ) + else -> this.size( + VideoTheme.dimens.screenShareParticipantItemSize * 1.5f, + VideoTheme.dimens.screenShareParticipantItemSize, + ) + } +} + +@Preview +@Composable +private fun SpotlightParticipantsPreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + SpotlightVideoRenderer( + call = mockCall, + speaker = mockParticipant, + participants = mockParticipantList, + ) + } +} + +@Preview +@Composable +private fun SpotlightTwoParticipantsPreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + SpotlightVideoRenderer( + call = mockCall, + speaker = mockParticipant, + participants = mockParticipantList.take(3), + ) + } +} + +@Preview( + device = Devices.AUTOMOTIVE_1024p, + widthDp = 1440, + heightDp = 720, +) +@Composable +private fun SpotlightParticipantsLandscapePreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + SpotlightVideoRenderer( + call = mockCall, + orientation = ORIENTATION_LANDSCAPE, + speaker = mockParticipant, + participants = mockParticipantList, + ) + } +} + +@Preview( + device = Devices.AUTOMOTIVE_1024p, + widthDp = 1440, + heightDp = 720, +) +@Composable +private fun SpotlightThreeParticipantsLandscapePreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + SpotlightVideoRenderer( + call = mockCall, + orientation = ORIENTATION_LANDSCAPE, + speaker = mockParticipant, + participants = mockParticipantList.take(3), + ) + } +} diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/extensions/ModifierExtensions.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/extensions/ModifierExtensions.kt index 2b8201598b..946fe37d63 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/extensions/ModifierExtensions.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/extensions/ModifierExtensions.kt @@ -16,12 +16,20 @@ package io.getstream.video.android.compose.ui.extensions +import androidx.compose.foundation.layout.padding import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.Dp import io.getstream.video.android.compose.theme.VideoTheme +/** + * Toggle alpha. + * Based on the [isEnabled] parameter the alpha value will be chosen from the theme. + * + * @param isEnabled if view is enabled + */ internal fun Modifier.toggleAlpha(isEnabled: Boolean): Modifier = composed( inspectorInfo = debugInspectorInfo { name = "toggleAlpha" @@ -35,3 +43,53 @@ internal fun Modifier.toggleAlpha(isEnabled: Boolean): Modifier = composed( } alpha(alpha) } + +/** + * Add padding to the modifier based on the index and its relative position in the list. + * + * + * If [first] equal to [index] then this boils down to: + * ``` + * modifier.padding(start = value) + * ``` + * If [last] equals to [index] then this is + * ``` + * modifier.padding(end = value) + * ``` + * Otherwise the modifier itself is returned. + */ +internal fun Modifier.startOrEndPadding( + value: Dp, + index: Int, + first: Int = 0, + last: Int, +): Modifier = when (index) { + first -> this.padding(start = value) + last -> this.padding(end = value) + else -> this +} + +/** + * Add padding to the modifier based on the index and its relative position in the list. + * + * + * If [first] equal to [index] then this boils down to: + * ``` + * modifier.padding(top = value) + * ``` + * If [last] equals to [index] then this is + * ``` + * modifier.padding(bottom = value) + * ``` + * Otherwise the modifier itself is returned. + */ +internal fun Modifier.topOrBottomPadding( + value: Dp, + index: Int, + first: Int = 0, + last: Int, +): Modifier = when (index) { + first -> this.padding(top = value) + last -> this.padding(bottom = value) + else -> this +} 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. */ diff --git a/stream-video-android-ui-common/src/main/res/values/dimens.xml b/stream-video-android-ui-common/src/main/res/values/dimens.xml index f45ca126ae..f6ea268a34 100644 --- a/stream-video-android-ui-common/src/main/res/values/dimens.xml +++ b/stream-video-android-ui-common/src/main/res/values/dimens.xml @@ -54,8 +54,8 @@ 140dp 16dp 16dp - 8dp - 1dp + 10dp + 2dp 2dp 22dp 2dp @@ -71,12 +71,12 @@ 8dp 4dp 8dp - 64dp + 120dp 4dp 110dp 125dp 8dp - 8dp + 4dp 16dp 16dp 16dp From 00b4ce4fded92a2ce9e4687a7d9e24ff474998a5 Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Tue, 31 Oct 2023 10:36:13 +0900 Subject: [PATCH 8/8] Prepare for release 0.4.1 (#894) --- .../main/kotlin/io/getstream/video/android/Configuration.kt | 6 +++--- docusaurus/docs/Android/02-tutorials/01-video-calling.mdx | 2 +- docusaurus/docs/Android/02-tutorials/02-audio-room.mdx | 2 +- docusaurus/docs/Android/02-tutorials/03-livestream.mdx | 2 +- docusaurus/docs/Android/06-advanced/07-chat-with-video.mdx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/buildSrc/src/main/kotlin/io/getstream/video/android/Configuration.kt b/buildSrc/src/main/kotlin/io/getstream/video/android/Configuration.kt index d4669ed884..593e3bc48a 100644 --- a/buildSrc/src/main/kotlin/io/getstream/video/android/Configuration.kt +++ b/buildSrc/src/main/kotlin/io/getstream/video/android/Configuration.kt @@ -6,10 +6,10 @@ object Configuration { const val minSdk = 24 const val majorVersion = 0 const val minorVersion = 4 - const val patchVersion = 0 + const val patchVersion = 1 const val versionName = "$majorVersion.$minorVersion.$patchVersion" - const val versionCode = 10 + const val versionCode = 11 const val snapshotVersionName = "$majorVersion.$minorVersion.${patchVersion + 1}-SNAPSHOT" const val artifactGroup = "io.getstream" - const val streamVideoCallGooglePlayVersion = "1.0.1" + const val streamVideoCallGooglePlayVersion = "1.0.2" } diff --git a/docusaurus/docs/Android/02-tutorials/01-video-calling.mdx b/docusaurus/docs/Android/02-tutorials/01-video-calling.mdx index 79802ecb6e..6c5b73fc07 100644 --- a/docusaurus/docs/Android/02-tutorials/01-video-calling.mdx +++ b/docusaurus/docs/Android/02-tutorials/01-video-calling.mdx @@ -31,7 +31,7 @@ If you're new to android, note that there are 2 `build.gradle` files, you want t ```kotlin dependencies { // Stream Video Compose SDK - implementation("io.getstream:stream-video-android-compose:0.4.0") + implementation("io.getstream:stream-video-android-compose:0.4.1") // Optionally add Jetpack Compose if Android studio didn't automatically include them implementation(platform("androidx.compose:compose-bom:2023.08.00")) diff --git a/docusaurus/docs/Android/02-tutorials/02-audio-room.mdx b/docusaurus/docs/Android/02-tutorials/02-audio-room.mdx index 66bd0858b2..d52bdc4b2b 100644 --- a/docusaurus/docs/Android/02-tutorials/02-audio-room.mdx +++ b/docusaurus/docs/Android/02-tutorials/02-audio-room.mdx @@ -35,7 +35,7 @@ If you're new to android, note that there are 2 `build.gradle` files, you want t ```groovy dependencies { // Stream Video Compose SDK - implementation("io.getstream:stream-video-android-compose:0.4.0") + implementation("io.getstream:stream-video-android-compose:0.4.1") // Jetpack Compose (optional/ android studio typically adds them when you create a new project) implementation(platform("androidx.compose:compose-bom:2023.08.00")) diff --git a/docusaurus/docs/Android/02-tutorials/03-livestream.mdx b/docusaurus/docs/Android/02-tutorials/03-livestream.mdx index 10ca8f9c0d..4d5377ed71 100644 --- a/docusaurus/docs/Android/02-tutorials/03-livestream.mdx +++ b/docusaurus/docs/Android/02-tutorials/03-livestream.mdx @@ -35,7 +35,7 @@ If you're new to android, note that there are 2 `build.gradle` files, you want t ```kotlin dependencies { // Stream Video Compose SDK - implementation("io.getstream:stream-video-android-compose:0.4.0") + implementation("io.getstream:stream-video-android-compose:0.4.1") // Jetpack Compose (optional/ android studio typically adds them when you create a new project) implementation(platform("androidx.compose:compose-bom:2023.08.00")) diff --git a/docusaurus/docs/Android/06-advanced/07-chat-with-video.mdx b/docusaurus/docs/Android/06-advanced/07-chat-with-video.mdx index 2f9400d1b0..00e335ba58 100644 --- a/docusaurus/docs/Android/06-advanced/07-chat-with-video.mdx +++ b/docusaurus/docs/Android/06-advanced/07-chat-with-video.mdx @@ -31,7 +31,7 @@ Let the project sync. It should have all the dependencies required for you to fi ```groovy dependencies { // Stream Video Compose SDK - implementation("io.getstream:stream-video-android-compose:0.4.0") + implementation("io.getstream:stream-video-android-compose:0.4.1") // Stream Chat implementation(libs.stream.chat.compose)