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 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/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/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/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) diff --git a/docusaurus/docs/Android/assets/spotlight_landscape.png b/docusaurus/docs/Android/assets/spotlight_landscape.png new file mode 100644 index 0000000000..b1a05068a5 Binary files /dev/null and b/docusaurus/docs/Android/assets/spotlight_landscape.png differ diff --git a/docusaurus/docs/Android/assets/spotlight_portrait.png b/docusaurus/docs/Android/assets/spotlight_portrait.png new file mode 100644 index 0000000000..24bd5229c1 Binary files /dev/null and b/docusaurus/docs/Android/assets/spotlight_portrait.png differ 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/call/CallScreen.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt index 7e36679168..1ea9e46711 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt @@ -54,12 +54,15 @@ import io.getstream.video.android.compose.ui.components.call.activecall.CallCont import io.getstream.video.android.compose.ui.components.call.controls.ControlActions import io.getstream.video.android.compose.ui.components.call.controls.actions.CancelCallAction import io.getstream.video.android.compose.ui.components.call.controls.actions.ChatDialogAction +import io.getstream.video.android.compose.ui.components.call.controls.actions.DefaultOnCallActionHandler import io.getstream.video.android.compose.ui.components.call.controls.actions.FlipCameraAction import io.getstream.video.android.compose.ui.components.call.controls.actions.SettingsAction import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleCameraAction import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleMicrophoneAction +import io.getstream.video.android.compose.ui.components.call.renderer.LayoutType import io.getstream.video.android.core.Call import io.getstream.video.android.core.RealtimeConnection +import io.getstream.video.android.core.call.state.ChooseLayout import io.getstream.video.android.mock.StreamMockUtils import io.getstream.video.android.mock.mockCall import kotlinx.coroutines.delay @@ -77,8 +80,10 @@ fun CallScreen( val isMicrophoneEnabled by call.microphone.isEnabled.collectAsState() val speakingWhileMuted by call.state.speakingWhileMuted.collectAsState() var isShowingSettingMenu by remember { mutableStateOf(false) } + var isShowingLayoutChooseMenu by remember { mutableStateOf(false) } var isShowingReactionsMenu by remember { mutableStateOf(false) } var isShowingAvailableDeviceMenu by remember { mutableStateOf(false) } + var layout by remember { mutableStateOf(LayoutType.DYNAMIC) } var unreadCount by remember { mutableIntStateOf(0) } val chatState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden, @@ -113,8 +118,18 @@ fun CallScreen( CallContent( modifier = Modifier.background(color = VideoTheme.colors.appBackground), call = call, + layout = layout, enableInPictureInPicture = true, enableDiagnostics = BuildConfig.DEBUG, + onCallAction = { + when (it) { + ChooseLayout -> isShowingLayoutChooseMenu = true + else -> DefaultOnCallActionHandler.onCallAction( + call, + it, + ) + } + }, onBackPressed = { if (chatState.currentValue == ModalBottomSheetValue.Expanded) { scope.launch { chatState.hide() } @@ -263,6 +278,17 @@ fun CallScreen( ) } + if (isShowingLayoutChooseMenu) { + LayoutChooser( + onLayoutChoice = { + layout = it + isShowingLayoutChooseMenu = false + }, + current = layout, + onDismiss = { isShowingLayoutChooseMenu = false }, + ) + } + if (isShowingAvailableDeviceMenu) { AvailableDeviceMenu( call = call, diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt new file mode 100644 index 0000000000..cb2592230b --- /dev/null +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalLayoutApi::class) + +package io.getstream.video.android.ui.call + +import android.content.res.Configuration +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AutoAwesome +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.ui.components.call.renderer.LayoutType +import io.getstream.video.android.mock.StreamMockUtils + +private data class LayoutChooserDataItem( + val which: LayoutType, + val text: String = "", +) + +private val layouts = arrayOf( + LayoutChooserDataItem(LayoutType.DYNAMIC, "Dynamic"), + LayoutChooserDataItem(LayoutType.SPOTLIGHT, "Spotlight"), + LayoutChooserDataItem(LayoutType.GRID, "Grid"), +) + +/** + * Reactions menu. The reaction menu is a dialog displaying the list of reactions found in + * [DefaultReactionsMenuData]. + * @param current + * @param onDismiss on dismiss listener. + */ +@Composable +internal fun LayoutChooser( + current: LayoutType, + onLayoutChoice: (LayoutType) -> Unit, + onDismiss: () -> Unit, +) { + Dialog(onDismiss) { + Row(Modifier.background(VideoTheme.colors.appBackground)) { + layouts.forEach { + LayoutItem( + current = current, + item = it, + onClicked = onLayoutChoice, + ) + } + } + } +} + +@Composable +private fun LayoutItem( + modifier: Modifier = Modifier, + current: LayoutType, + item: LayoutChooserDataItem, + onClicked: (LayoutType) -> Unit = {}, +) { + val border = + if (current == item.which) BorderStroke(2.dp, VideoTheme.colors.primaryAccent) else null + Card( + modifier = modifier + .clickable { onClicked(item.which) } + .padding(12.dp), + backgroundColor = VideoTheme.colors.appBackground, + elevation = 3.dp, + border = border, + ) { + Column { + Box( + modifier = Modifier + .size(84.dp, 84.dp) + .padding(2.dp), + ) { + when (item.which) { + LayoutType.DYNAMIC -> { + DynamicRepresentation() + } + + LayoutType.SPOTLIGHT -> { + SpotlightRepresentation() + } + + LayoutType.GRID -> { + GridRepresentation() + } + } + } + Text( + modifier = Modifier + .align(CenterHorizontally) + .padding(12.dp), + textAlign = TextAlign.Center, + text = item.text, + color = VideoTheme.colors.textHighEmphasis, + ) + } + } +} + +@Composable +private fun DynamicRepresentation() { + Column { + Card( + modifier = Modifier + .weight(2f) + .fillMaxWidth() + .padding(2.dp), + backgroundColor = VideoTheme.colors.participantContainerBackground, + ) { + } + + Row(modifier = Modifier.weight(1f)) { + Card( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .padding(2.dp), + backgroundColor = VideoTheme.colors.participantContainerBackground, + ) { + } + Card( + backgroundColor = VideoTheme.colors.appBackground, + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .padding(2.dp), + ) { + Icon( + modifier = Modifier + .fillMaxSize() + .padding(2.dp), + tint = VideoTheme.colors.participantContainerBackground, + imageVector = Icons.Rounded.AutoAwesome, + contentDescription = "dynamic", + ) + } + } + } +} + +@Composable +private fun GridRepresentation() { + Column { + repeat(3) { + Row { + repeat(3) { + Card( + modifier = Modifier + .aspectRatio(1f) + .weight(1f) + .padding(2.dp), + backgroundColor = VideoTheme.colors.participantContainerBackground, + ) { + } + } + } + } + } +} + +@Composable +private fun SpotlightRepresentation() { + Column { + Card( + modifier = Modifier + .weight(2f) + .fillMaxWidth() + .padding(2.dp), + backgroundColor = VideoTheme.colors.participantContainerBackground, + ) { + } + + Row(modifier = Modifier.weight(1f)) { + repeat(3) { + Card( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .padding(2.dp), + backgroundColor = VideoTheme.colors.participantContainerBackground, + ) { + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun LayoutChooserPreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutChooser( + current = LayoutType.GRID, + onLayoutChoice = {}, + onDismiss = {}, + ) + } +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun LayoutChooserPreviewDark() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutChooser( + current = LayoutType.GRID, + onLayoutChoice = {}, + onDismiss = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun GridItemPreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutItem( + current = LayoutType.GRID, + item = layouts[2], + ) + } +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun GridItemPreviewDark() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutItem( + current = LayoutType.GRID, + item = layouts[2], + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun SpotlightItemPreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutItem( + current = LayoutType.GRID, + item = layouts[1], + ) + } +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun SpotlightItemPreviewDark() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutItem( + current = LayoutType.GRID, + item = layouts[1], + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun DynamicItemPreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutItem( + current = LayoutType.GRID, + item = layouts[0], + ) + } +} + +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun DynamicItemPreviewDark() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LayoutItem( + current = LayoutType.GRID, + item = layouts[0], + ) + } +} diff --git a/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 = { }, ) } } 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" } 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..2bbc14ea14 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 { @@ -603,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 { @@ -613,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 { @@ -725,7 +749,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 { @@ -802,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 @@ -827,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 @@ -838,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 { @@ -850,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 @@ -906,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 @@ -1017,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 } @@ -1138,6 +1225,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/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-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/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/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 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..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 @@ -46,13 +46,15 @@ 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.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 @@ -106,7 +108,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 +141,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 +159,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 +296,42 @@ public fun BoxScope.ParticipantLabel( ) }, ) { - Row( + 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) - .padding(VideoTheme.dimens.participantLabelPadding) .height(VideoTheme.dimens.participantLabelHeight) .wrapContentWidth() - .clip(RoundedCornerShape(8.dp)) .background( VideoTheme.colors.participantLabelBackground, - shape = RoundedCornerShape(8.dp), - ), - verticalAlignment = CenterVertically, + shape = VideoTheme.shapes.participantLabelShape, + ).onGloballyPositioned { + componentWidth = with(density) { + it.size.width.toDp() + } + }, ) { - 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 = componentWidth) + .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/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 2fd3eb3b34..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 @@ -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, @@ -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/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..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 @@ -25,8 +25,10 @@ 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.foundation.lazy.grid.rememberLazyGridState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -84,14 +86,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 +105,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,187 +123,38 @@ 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, + ) } } else -> { BoxWithConstraints(modifier = Modifier.fillMaxHeight()) { - val gridState = lazyGridStateWithVisibilityNotification(call = call) + val gridState = + lazyStateWithVisibilityNotification( + call = call, + original = rememberLazyGridState(), + ) LazyVerticalGrid( modifier = Modifier.fillMaxSize(), state = gridState, @@ -315,7 +170,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 +184,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 +202,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/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 01c0f7ffac..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?, @@ -116,12 +127,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/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 03a509ecdc..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 @@ -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 = modifier, + call = call, + session = session, + isZoomable = isZoomable, + me = me, + sharingParticipant = 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..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 @@ -24,8 +24,10 @@ 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.foundation.lazy.grid.rememberLazyGridState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -89,12 +91,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,224 +109,52 @@ 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) + val gridState = + lazyStateWithVisibilityNotification( + call = call, + original = rememberLazyGridState(), + ) LazyVerticalGrid( modifier = Modifier.fillMaxSize(), columns = GridCells.Fixed(2), @@ -336,7 +170,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 +184,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 +202,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/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/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-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 5293159365..3eb1a5d911 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; @@ -1009,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/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/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-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()) + } +} 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..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 @@ -53,27 +53,30 @@ 200dp 140dp 16dp - 14dp - 3dp - 3dp - 16dp - 3dp - 3dp - 15dp - 6dp - 4dp - 6dp - 4dp - 24dp + 16dp + 10dp + 2dp + 2dp + 22dp + 2dp + 1dp + 24dp + 18dp + 2dp + 2dp + 1dp + 2dp + 2dp + 32dp 8dp - 0dp + 4dp 8dp - 64dp + 120dp 4dp 110dp 125dp 8dp - 8dp + 4dp 16dp 16dp 16dp @@ -85,10 +88,12 @@ 280dp 12dp 32dp - 45dp + 24dp 44dp 84dp - 4dp + 8dp + 4dp + 5dp 20dp 5dp 22dp