diff --git a/README.md b/README.md index 8f1c1ab764..e26a6c6909 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ Stream provides UI components and state handling that make it easy to build vide With Stream's video components, you can use their SDK to build in-app video calling, audio rooms, audio calls, or live streaming. The best place to get started is with their tutorials: -- **[Video & Audio Calling Tutorial](https://getstream.io/video/docs/android/tutorials/video-calling/)** -- **[Audio Rooms Tutorial](https://getstream.io/video/docs/android/tutorials/audio-room/)** -- **[Livestreaming Tutorial](https://getstream.io/video/docs/android/tutorials/livestream/)** +- **[Video & Audio Calling Tutorial](https://getstream.io/video/docs/android/tutorials/video-calling?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Android_Video_SDK&utm_term=DevRelOss)** +- **[Audio Rooms Tutorial](https://getstream.io/video/docs/android/tutorials/audio-room?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Android_Video_SDK&utm_term=DevRelOss)** +- **[Livestreaming Tutorial](https://getstream.io/video/docs/android/tutorials/livestream?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Android_Video_SDK&utm_term=DevRelOss)** -If you're interested in customizing the UI components for the Video SDK, check out the **[UI Cookbook](https://getstream.io/video/docs/android/ui-cookbook/overview/)**. +If you're interested in customizing the UI components for the Video SDK, check out the **[UI Cookbook](https://getstream.io/video/docs/android/ui-cookbook/overview?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Android_Video_SDK&utm_term=DevRelOss)**. ## 📱 Previews @@ -43,7 +43,7 @@ If you're interested in customizing the UI components for the Video SDK, check o You can find sample projects below that demonstrates use cases of Stream Video SDK for Android: -- [Dogfooding](https://github.com/GetStream/stream-video-android/tree/develop/dogfooding): Dogfooding demonstrates Stream Video SDK for Android with modern Android tech stacks, such as Compose, Hilt, and Coroutines. +- [Demo App](https://github.com/GetStream/stream-video-android/tree/develop/demo-app): Demo App demonstrates Stream Video SDK for Android with modern Android tech stacks, such as Compose, Hilt, and Coroutines. - [WhatsApp Clone Compose](https://github.com/getstream/whatsapp-clone-compose): WhatsApp clone project demonstrates modern Android development built with Jetpack Compose and Stream Chat/Video SDK for Compose. - [Twitch Clone Compose](https://github.com/skydoves/twitch-clone-compose): Twitch clone project demonstrates modern Android development built with Jetpack Compose and Stream Chat/Video SDK for Compose. - [Meeting Room Compose](https://github.com/GetStream/meeting-room-compose): A real-time meeting room app built with Jetpack Compose to demonstrate video communications. @@ -169,7 +169,7 @@ Check out our current openings and apply via [Stream's website](https://getstrea ## License ``` -Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. +Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. Licensed under the Stream License; you may not use this file except in compliance with the License. diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts index b81c9e0db0..1389a3e8a2 100644 --- a/benchmark/build.gradle.kts +++ b/benchmark/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/benchmark/src/main/AndroidManifest.xml b/benchmark/src/main/AndroidManifest.xml index 3e22367c5e..526090f675 100644 --- a/benchmark/src/main/AndroidManifest.xml +++ b/benchmark/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + @@ -89,6 +91,8 @@ + + diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/App.kt b/demo-app/src/main/kotlin/io/getstream/video/android/App.kt index 62a7760cca..4353cca61d 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/App.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/App.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/DeeplinkingActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/DeeplinkingActivity.kt index be0dc79830..ed9b0e0fdc 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/DeeplinkingActivity.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/DeeplinkingActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -36,14 +36,15 @@ import dagger.hilt.android.AndroidEntryPoint import io.getstream.android.push.permissions.NotificationPermissionManager import io.getstream.android.push.permissions.NotificationPermissionStatus import io.getstream.log.taggedLogger -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.StreamVideo import io.getstream.video.android.datastore.delegate.StreamUserDataStore import io.getstream.video.android.model.StreamCallId import io.getstream.video.android.ui.call.CallActivity -import io.getstream.video.android.ui.theme.Colors import io.getstream.video.android.util.InitializedState import io.getstream.video.android.util.StreamVideoInitHelper +import io.getstream.video.android.util.config.AppConfig +import io.getstream.video.android.util.config.AppConfig.fromUri import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import javax.inject.Inject @@ -65,11 +66,11 @@ class DeeplinkingActivity : ComponentActivity() { Box( modifier = Modifier .fillMaxSize() - .background(Colors.background), + .background(VideoTheme.colors.baseSheetPrimary), ) { CircularProgressIndicator( modifier = Modifier.align(Alignment.Center), - color = VideoTheme.colors.primaryAccent, + color = VideoTheme.colors.brandPrimary, ) } } @@ -106,7 +107,7 @@ class DeeplinkingActivity : ComponentActivity() { ) == PackageManager.PERMISSION_GRANTED ) { // ensure that audio & video permissions are granted - joinCall(callId) + joinCall(data, callId) } else { // first ask for push notification permission val manager = NotificationPermissionManager.createNotificationPermissionsManager( @@ -115,7 +116,7 @@ class DeeplinkingActivity : ComponentActivity() { onPermissionStatus = { // we don't care about the result for demo purposes if (it != NotificationPermissionStatus.REQUESTED) { - joinCall(callId) + joinCall(data, callId) } }, ) @@ -158,11 +159,17 @@ class DeeplinkingActivity : ComponentActivity() { return callId ?: data.getQueryParameter("id") } - private fun joinCall(cid: String) { + private fun joinCall(data: Uri?, cid: String) { lifecycleScope.launch { + data?.let { + val determinedEnv = AppConfig.availableEnvironments.fromUri(it) + determinedEnv?.let { + AppConfig.selectEnv(determinedEnv) + } + } // Deep link can be opened without the app after install - there is no user yet // But in this case the StreamVideoInitHelper will use a random account - StreamVideoInitHelper.loadSdk( + StreamVideoInitHelper.reloadSdk( dataStore = dataStore, useRandomUserAsFallback = true, ) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/DirectCallActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/DirectCallActivity.kt index 5446fedcaf..d31d15d14a 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/DirectCallActivity.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/DirectCallActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ 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.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.call.ringing.RingingCallContent import io.getstream.video.android.core.Call import io.getstream.video.android.core.StreamVideo @@ -143,7 +143,7 @@ class DirectCallActivity : ComponentActivity() { } RingingCallContent( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), call = call, onBackPressed = { reject(call) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/IncomingCallActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/IncomingCallActivity.kt index 629fecdf0b..d7ed0aa593 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/IncomingCallActivity.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/IncomingCallActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ 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.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.call.ringing.RingingCallContent import io.getstream.video.android.core.StreamVideo import io.getstream.video.android.core.call.state.AcceptCall @@ -121,7 +121,7 @@ class IncomingCallActivity : ComponentActivity() { } } RingingCallContent( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), call = call, onBackPressed = { call.leave() diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/MainActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/MainActivity.kt index ce2e566bd3..d2feac16fb 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/MainActivity.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/MainActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import androidx.lifecycle.repeatOnLifecycle import com.google.firebase.analytics.FirebaseAnalytics import dagger.hilt.android.AndroidEntryPoint import io.getstream.video.android.analytics.FirebaseEvents -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.datastore.delegate.StreamUserDataStore import io.getstream.video.android.tooling.util.StreamFlavors import io.getstream.video.android.ui.AppNavHost diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/analytics/FirebaseEvents.kt b/demo-app/src/main/kotlin/io/getstream/video/android/analytics/FirebaseEvents.kt index f3cd94654f..88cabc1c92 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/analytics/FirebaseEvents.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/analytics/FirebaseEvents.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/data/repositories/GoogleAccountRepository.kt b/demo-app/src/main/kotlin/io/getstream/video/android/data/repositories/GoogleAccountRepository.kt index 282a091571..5b2c940334 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/data/repositories/GoogleAccountRepository.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/data/repositories/GoogleAccountRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/data/services/google/ListDirectoryPeopleResponse.kt b/demo-app/src/main/kotlin/io/getstream/video/android/data/services/google/ListDirectoryPeopleResponse.kt index f7584582dc..d6c469629a 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/data/services/google/ListDirectoryPeopleResponse.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/data/services/google/ListDirectoryPeopleResponse.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/data/services/stream/GetAuthDataResponse.kt b/demo-app/src/main/kotlin/io/getstream/video/android/data/services/stream/GetAuthDataResponse.kt index 385590d20a..86e6f4cc8c 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/data/services/stream/GetAuthDataResponse.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/data/services/stream/GetAuthDataResponse.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/data/services/stream/StreamService.kt b/demo-app/src/main/kotlin/io/getstream/video/android/data/services/stream/StreamService.kt index 9189f4dcc9..a50e5b07fb 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/data/services/stream/StreamService.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/data/services/stream/StreamService.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/di/AppModule.kt b/demo-app/src/main/kotlin/io/getstream/video/android/di/AppModule.kt index 4e06543279..1cec7a156b 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/di/AppModule.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/di/AppModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/models/GoogleAccount.kt b/demo-app/src/main/kotlin/io/getstream/video/android/models/GoogleAccount.kt index befc88a1d4..af0a717110 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/models/GoogleAccount.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/models/GoogleAccount.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/tooling/extensions/ContextExtensions.kt b/demo-app/src/main/kotlin/io/getstream/video/android/tooling/extensions/ContextExtensions.kt index 7c9d4bf7aa..a6846bd918 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/tooling/extensions/ContextExtensions.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/tooling/extensions/ContextExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/SelectedCallParticipantOptions.kt b/demo-app/src/main/kotlin/io/getstream/video/android/tooling/extensions/Utils.kt similarity index 67% rename from stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/SelectedCallParticipantOptions.kt rename to demo-app/src/main/kotlin/io/getstream/video/android/tooling/extensions/Utils.kt index 44be317e68..3d670109d4 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/SelectedCallParticipantOptions.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/tooling/extensions/Utils.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -14,10 +14,14 @@ * limitations under the License. */ -package io.getstream.video.android.compose.ui.components.participants.internal +package io.getstream.video.android.tooling.extensions import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp @Composable -internal fun SelectedCallParticipantOptions() { // TODO - show some options based on permissions +fun Dp.toPx(): Float { + val density = LocalDensity.current.density + return this.value * density } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/tooling/ui/ExceptionTraceActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/tooling/ui/ExceptionTraceActivity.kt index 5c79581b5e..903865bbd7 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/tooling/ui/ExceptionTraceActivity.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/tooling/ui/ExceptionTraceActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/tooling/ui/ExceptionTraceScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/tooling/ui/ExceptionTraceScreen.kt index 2eecdb7a6b..2a7a228073 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/tooling/ui/ExceptionTraceScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/tooling/ui/ExceptionTraceScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -47,7 +47,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.getstream.video.android.R -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamButton import io.getstream.video.android.tooling.extensions.toast @Composable @@ -55,13 +56,15 @@ internal fun ExceptionTraceScreen(packageName: String, message: String) { val scrollState = rememberScrollState() Column( modifier = - Modifier.verticalScroll(scrollState) - .background(VideoTheme.colors.appBackground) + Modifier + .verticalScroll(scrollState) + .background(VideoTheme.colors.baseSheetPrimary) .padding(16.dp), ) { val context: Context = LocalContext.current - StreamPrimaryButton( - text = R.string.stream_video_tooling_restart_app, + StreamButton( + style = VideoTheme.styles.buttonStyles.secondaryButtonStyle(), + text = stringResource(id = R.string.stream_video_tooling_restart_app), onClick = { val mainActivity = Class.forName(packageName) context.startActivity(Intent(context, mainActivity)) @@ -72,7 +75,7 @@ internal fun ExceptionTraceScreen(packageName: String, message: String) { Text( modifier = Modifier.align(Alignment.CenterStart), text = stringResource(id = R.string.stream_video_tooling_exception_log), - color = VideoTheme.colors.primaryAccent, + color = VideoTheme.colors.basePrimary, fontWeight = FontWeight.Bold, fontSize = 16.sp, ) @@ -80,12 +83,14 @@ internal fun ExceptionTraceScreen(packageName: String, message: String) { val clipboardManager: ClipboardManager = LocalClipboardManager.current Icon( modifier = - Modifier.align(Alignment.CenterEnd).clickable { - clipboardManager.setText(AnnotatedString(message)) - context.toast(R.string.stream_video_tooling_copy_into_clipboard) - }, + Modifier + .align(Alignment.CenterEnd) + .clickable { + clipboardManager.setText(AnnotatedString(message)) + context.toast(R.string.stream_video_tooling_copy_into_clipboard) + }, imageVector = Icons.Filled.ContentCopy, - tint = VideoTheme.colors.textHighEmphasis, + tint = VideoTheme.colors.basePrimary, contentDescription = null, ) } @@ -94,13 +99,14 @@ internal fun ExceptionTraceScreen(packageName: String, message: String) { Text( modifier = - Modifier.border( - border = BorderStroke(2.dp, VideoTheme.colors.primaryAccent), - shape = RoundedCornerShape(6.dp), - ) + Modifier + .border( + border = BorderStroke(2.dp, VideoTheme.colors.brandPrimary), + shape = RoundedCornerShape(6.dp), + ) .padding(12.dp), text = message, - color = VideoTheme.colors.textHighEmphasis, + color = VideoTheme.colors.basePrimary, fontSize = 14.sp, ) } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/tooling/ui/StreamPrimaryButton.kt b/demo-app/src/main/kotlin/io/getstream/video/android/tooling/ui/StreamPrimaryButton.kt deleted file mode 100644 index c80f3435de..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/tooling/ui/StreamPrimaryButton.kt +++ /dev/null @@ -1,69 +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.tooling.ui - -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import io.getstream.video.android.compose.theme.VideoTheme - -@Composable -internal fun StreamPrimaryButton( - modifier: Modifier = Modifier, - @StringRes text: Int = -1, - onClick: () -> Unit, - enabled: Boolean = true, - contentColor: Color? = null, - disabledContentColor: Color? = null, - content: @Composable (RowScope.() -> Unit)? = null, -) { - Button( - modifier = modifier.fillMaxWidth().padding(16.dp).heightIn(min = 54.dp), - shape = RoundedCornerShape(8.dp), - enabled = enabled, - colors = - ButtonDefaults.buttonColors( - contentColor = contentColor ?: VideoTheme.colors.primaryAccent, - backgroundColor = contentColor ?: VideoTheme.colors.primaryAccent, - disabledContentColor = disabledContentColor ?: VideoTheme.colors.disabled, - ), - onClick = onClick, - content = content - ?: { - Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = text), - color = Color.White, - textAlign = TextAlign.Center, - fontSize = 16.sp, - ) - }, - ) -} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/tooling/util/StreamFlavors.kt b/demo-app/src/main/kotlin/io/getstream/video/android/tooling/util/StreamFlavors.kt index a92924fd7e..78795aa67e 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/tooling/util/StreamFlavors.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/tooling/util/StreamFlavors.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt index 248176ec90..26fc22eaa0 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -77,10 +77,8 @@ fun AppNavHost( arguments = listOf(navArgument("cid") { type = NavType.StringType }), ) { CallLobbyScreen( - navigateUpToLogin = { - navController.navigate(AppScreens.Login.route) { - popUpTo(AppScreens.CallJoin.route) { inclusive = true } - } + onBack = { + navController.popBackStack() }, ) } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/AvailableDeviceMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/AvailableDeviceMenu.kt index 86a1eeede5..67297cc7d9 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/AvailableDeviceMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/AvailableDeviceMenu.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.Call import kotlinx.coroutines.delay @@ -66,8 +66,8 @@ fun AvailableDeviceMenu( Card( modifier = Modifier.width(140.dp), shape = RoundedCornerShape(12.dp), - contentColor = VideoTheme.colors.appBackground, - backgroundColor = VideoTheme.colors.appBackground, + contentColor = VideoTheme.colors.basePrimary, + backgroundColor = VideoTheme.colors.baseSheetPrimary, elevation = 6.dp, ) { LazyColumn( @@ -90,7 +90,7 @@ fun AvailableDeviceMenu( .show() }, text = audioDevice.name, - color = VideoTheme.colors.textHighEmphasis, + color = VideoTheme.colors.basePrimary, ) } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallActivity.kt index 98d02a5054..de4e534b9b 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallActivity.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt index 253c039323..dd246511c1 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -18,19 +18,29 @@ package io.getstream.video.android.ui.call +import android.content.ClipboardManager +import android.content.Context +import android.content.res.Configuration import android.widget.Toast import androidx.compose.animation.Crossfade import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.PaddingValues +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.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.Badge import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Snackbar import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.People +import androidx.compose.material.icons.filled.RadioButtonChecked import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -41,29 +51,40 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState 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.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.getstream.chat.android.ui.common.state.messages.list.MessageItemState import io.getstream.video.android.BuildConfig -import io.getstream.video.android.compose.theme.StreamDimens -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.R +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamBadgeBox +import io.getstream.video.android.compose.ui.components.base.StreamDialogPositiveNegative +import io.getstream.video.android.compose.ui.components.base.StreamIconToggleButton +import io.getstream.video.android.compose.ui.components.call.CallAppBar import io.getstream.video.android.compose.ui.components.call.activecall.CallContent -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.GenericAction +import io.getstream.video.android.compose.ui.components.call.controls.actions.LeaveCallAction +import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleAction 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.controls.actions.ToggleSettingsAction import io.getstream.video.android.compose.ui.components.call.renderer.FloatingParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.LayoutType import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantVideo @@ -74,10 +95,16 @@ import io.getstream.video.android.core.RealtimeConnection import io.getstream.video.android.core.call.state.ChooseLayout import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewCall +import io.getstream.video.android.tooling.extensions.toPx import io.getstream.video.android.tooling.util.StreamFlavors +import io.getstream.video.android.ui.menu.SettingsMenu +import io.getstream.video.android.util.config.AppConfig +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.openapitools.client.models.OwnCapability +@OptIn(ExperimentalMaterialApi::class) @Composable fun CallScreen( call: Call, @@ -94,17 +121,28 @@ fun CallScreen( var isShowingReactionsMenu by remember { mutableStateOf(false) } var isShowingAvailableDeviceMenu by remember { mutableStateOf(false) } var isBackgroundBlurEnabled by remember { mutableStateOf(false) } + var isShowingFeedbackDialog by remember { mutableStateOf(false) } var isShowingStats by remember { mutableStateOf(false) } var layout by remember { mutableStateOf(LayoutType.DYNAMIC) } var unreadCount by remember { mutableIntStateOf(0) } + var showParticipants by remember { mutableStateOf(false) } val chatState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden, skipHalfExpanded = true, ) + var showRecordingWarning by remember { + mutableStateOf(false) + } + val orientation = LocalConfiguration.current.orientation + var showEndRecordingDialog by remember { mutableStateOf(false) } + var acceptedCallRecording by remember { mutableStateOf(false) } + val isRecording by call.state.recording.collectAsStateWithLifecycle() + val participantsSize by call.state.participants.collectAsStateWithLifecycle() val messages: MutableList = remember { mutableStateListOf() } var messagesVisibility by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() val messageScope = rememberCoroutineScope() + var showingLandscapeControls by remember { mutableStateOf(false) } val connection by call.state.connection.collectAsStateWithLifecycle() val me by call.state.me.collectAsState() @@ -121,22 +159,29 @@ fun CallScreen( onCallDisconnected.invoke() } } + val paddings = if (orientation == Configuration.ORIENTATION_PORTRAIT) { + PaddingValues(start = 4.dp, end = 4.dp, top = 8.dp, bottom = 16.dp) + } else { + PaddingValues(0.dp) + } - VideoTheme( - dimens = StreamDimens.defaultDimens().copy(reactionSize = 32.dp), - ) { + VideoTheme { ChatDialog( state = chatState, call = call, content = { BoxWithConstraints(modifier = Modifier.fillMaxSize()) { CallContent( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier + .fillMaxSize() + .padding(paddings) + .background( + color = VideoTheme.colors.baseSheetPrimary, + ), call = call, layout = layout, enableInPictureInPicture = true, - enableDiagnostics = BuildConfig.DEBUG || - BuildConfig.FLAVOR == StreamFlavors.development, + enableDiagnostics = BuildConfig.DEBUG || BuildConfig.FLAVOR == StreamFlavors.development, onCallAction = { when (it) { ChooseLayout -> isShowingLayoutChooseMenu = true @@ -146,6 +191,42 @@ fun CallScreen( ) } }, + appBarContent = { + CallAppBar( + modifier = Modifier.padding(horizontal = 8.dp), + call = call, + leadingContent = { + val iconOnOff = ImageVector.vectorResource( + R.drawable.ic_layout_grid, + ) + Row { + ToggleAction( + offStyle = VideoTheme.styles.buttonStyles.secondaryIconButtonStyle(), + isActionActive = !isShowingLayoutChooseMenu, + iconOnOff = Pair(iconOnOff, iconOnOff), + ) { + isShowingLayoutChooseMenu = + !isShowingLayoutChooseMenu + } + + Spacer( + modifier = Modifier.size( + VideoTheme.dimens.spacingM, + ), + ) + + FlipCameraAction( + onCallAction = { call.camera.flip() }, + ) + } + }, + trailingContent = { + LeaveCallAction { + call.leave() + } + }, + ) + }, onBackPressed = { if (chatState.currentValue == ModalBottomSheetValue.Expanded) { scope.launch { chatState.hide() } @@ -154,85 +235,78 @@ fun CallScreen( } }, controlsContent = { - ControlActions( - call = call, - actions = listOf( - { - SettingsAction( - modifier = Modifier.size( - VideoTheme.dimens.controlActionsButtonSize, - ), - onCallAction = { isShowingSettingMenu = true }, + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row { + ToggleSettingsAction( + isShowingSettings = !isShowingSettingMenu, + onCallAction = { + isShowingSettingMenu = !isShowingSettingMenu + }, + ) + Spacer(modifier = Modifier.size(VideoTheme.dimens.spacingM)) + if (call.hasCapability(OwnCapability.StartRecordCall) || call.hasCapability( + OwnCapability.StopRecordCall, ) - }, - { - Box( - modifier = Modifier.size( - VideoTheme.dimens.controlActionsButtonSize, + ) { + ToggleAction( + progress = showEndRecordingDialog, + isActionActive = !isRecording, + iconOnOff = Pair( + Icons.Default.RadioButtonChecked, + Icons.Default.RadioButtonChecked, ), - ) { - ChatDialogAction( - modifier = Modifier.size( - VideoTheme.dimens.controlActionsButtonSize, - ), - onCallAction = { scope.launch { chatState.show() } }, - ) - - if (unreadCount > 0) { - Badge( - modifier = Modifier.align(Alignment.TopEnd), - backgroundColor = VideoTheme.colors.errorAccent, - contentColor = VideoTheme.colors.errorAccent, - ) { - Text( - text = unreadCount.toString(), - color = VideoTheme.colors.textHighEmphasis, - fontWeight = FontWeight.Bold, - ) + onAction = { + GlobalScope.launch { + if (isRecording) { + showEndRecordingDialog = true + } else { + call.startRecording() + } } - } - } - }, - { - ToggleCameraAction( - modifier = Modifier.size( - VideoTheme.dimens.controlActionsButtonSize, - ), - isCameraEnabled = isCameraEnabled, - onCallAction = { call.camera.setEnabled(it.isEnabled) }, - ) - }, - { - ToggleMicrophoneAction( - modifier = Modifier.size( - VideoTheme.dimens.controlActionsButtonSize, - ), - isMicrophoneEnabled = isMicrophoneEnabled, - onCallAction = { - call.microphone.setEnabled( - it.isEnabled, - ) }, ) - }, - { - FlipCameraAction( - modifier = Modifier.size( - VideoTheme.dimens.controlActionsButtonSize, - ), - onCallAction = { call.camera.flip() }, - ) - }, - { - CancelCallAction( + Spacer( modifier = Modifier.size( - VideoTheme.dimens.controlActionsButtonSize, + VideoTheme.dimens.spacingM, ), - onCallAction = { onUserLeaveCall.invoke() }, ) - }, - ), - ) + } + ToggleMicrophoneAction( + isMicrophoneEnabled = isMicrophoneEnabled, + onCallAction = { + call.microphone.setEnabled( + it.isEnabled, + ) + }, + ) + Spacer(modifier = Modifier.size(VideoTheme.dimens.spacingM)) + ToggleCameraAction( + isCameraEnabled = isCameraEnabled, + onCallAction = { call.camera.setEnabled(it.isEnabled) }, + ) + Spacer(modifier = Modifier.size(VideoTheme.dimens.spacingM)) + } + Row { + StreamBadgeBox( + text = participantsSize.size.toString(), + ) { + GenericAction(icon = Icons.Default.People) { + showParticipants = !showParticipants + } + } + Spacer(modifier = Modifier.size(VideoTheme.dimens.spacingM)) + ChatDialogAction( + messageCount = unreadCount, + onCallAction = { scope.launch { chatState.show() } }, + ) + } + } }, videoRenderer = { modifier, call, participant, style -> ParticipantVideo( @@ -263,7 +337,7 @@ fun CallScreen( ParticipantVideo( modifier = Modifier .fillMaxSize() - .clip(VideoTheme.shapes.floatingParticipant), + .clip(VideoTheme.shapes.dialog), call = call, participant = participant, reactionContent = { @@ -282,12 +356,7 @@ fun CallScreen( }, videoOverlayContent = { Crossfade( - modifier = Modifier - .align(Alignment.BottomStart) - .padding( - horizontal = VideoTheme.dimens.participantLabelPadding, - vertical = VideoTheme.dimens.participantLabelHeight + 8.dp, - ), + modifier = Modifier.align(Alignment.BottomStart), targetState = messagesVisibility, label = "chat_overlay", ) { visibility -> @@ -297,6 +366,27 @@ fun CallScreen( } }, ) + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + StreamIconToggleButton( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(VideoTheme.dimens.spacingM), + toggleState = rememberUpdatedState( + newValue = ToggleableState( + showingLandscapeControls, + ), + ), + onIcon = Icons.Default.MoreVert, + onStyle = VideoTheme.styles.buttonStyles.secondaryIconButtonStyle(), + offStyle = VideoTheme.styles.buttonStyles.tetriaryIconButtonStyle(), + ) { + showingLandscapeControls = when (it) { + ToggleableState.On -> false + ToggleableState.Off -> true + ToggleableState.Indeterminate -> false + } + } + } } }, updateUnreadCount = { unreadCount = it }, @@ -318,32 +408,65 @@ fun CallScreen( }, ) + if (participantsSize.size == 1 && !chatState.isVisible && orientation == Configuration.ORIENTATION_PORTRAIT) { + val context = LocalContext.current + val clipboardManager = remember(context) { + context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager + } + val env = AppConfig.currentEnvironment.collectAsStateWithLifecycle() + Popup( + alignment = Alignment.BottomCenter, + offset = IntOffset( + 0, + -(VideoTheme.dimens.componentHeightL + VideoTheme.dimens.spacingS).toPx() + .toInt(), + ), + ) { + ShareCallWithOthers( + modifier = Modifier.fillMaxWidth(), + call = call, + clipboardManager = clipboardManager, + env = env, + context = context, + ) + } + } + if (speakingWhileMuted) { SpeakingWhileMuted() } + if (showingLandscapeControls && orientation == Configuration.ORIENTATION_LANDSCAPE) { + LandscapeControls(call) { + showingLandscapeControls = !showingLandscapeControls + } + } + if (isShowingSettingMenu) { SettingsMenu( call = call, showDebugOptions = showDebugOptions, isBackgroundBlurEnabled = isBackgroundBlurEnabled, - onDisplayAvailableDevice = { isShowingAvailableDeviceMenu = true }, onDismissed = { isShowingSettingMenu = false }, - onShowReactionsMenu = { isShowingReactionsMenu = true }, + onShowFeedback = { + isShowingSettingMenu = false + isShowingFeedbackDialog = true + }, onToggleBackgroundBlur = { isBackgroundBlurEnabled = !isBackgroundBlurEnabled isShowingSettingMenu = false }, - onShowCallStats = { isShowingStats = true }, + onShowCallStats = { + isShowingStats = true + isShowingSettingMenu = false + }, ) } - if (isShowingReactionsMenu) { - ReactionsMenu( - call = call, - reactionMapper = VideoTheme.reactionMapper, - onDismiss = { isShowingReactionsMenu = false }, - ) + if (isShowingFeedbackDialog) { + FeedbackDialog(call = call) { + isShowingFeedbackDialog = false + } } if (isShowingLayoutChooseMenu) { @@ -361,12 +484,62 @@ fun CallScreen( CallStatsDialog(call) { isShowingStats = false } } + if (showParticipants) { + ParticipantsDialog(call) { + showParticipants = !showParticipants + } + } + if (isShowingAvailableDeviceMenu) { AvailableDeviceMenu( call = call, onDismissed = { isShowingAvailableDeviceMenu = false }, ) } + + // TODO: AAP, move recording and actions in separate composables. + if (isRecording && !showRecordingWarning) { + StreamDialogPositiveNegative( + title = "This call is being recorded", + contentText = "By staying in the call you’re consenting to being recorded.", + positiveButton = Triple( + "Continue", + VideoTheme.styles.buttonStyles.secondaryButtonStyle(), + ) { + showRecordingWarning = true + acceptedCallRecording = true + }, + negativeButton = Triple( + "Leave", + VideoTheme.styles.buttonStyles.tetriaryButtonStyle(), + ) { + showRecordingWarning = false + acceptedCallRecording = false + call.leave() + }, + ) + } + if (showEndRecordingDialog) { + StreamDialogPositiveNegative( + title = "End recording", + contentText = "Are you sure you want to end the recording?", + positiveButton = Triple( + "End", + VideoTheme.styles.buttonStyles.alertButtonStyle(), + ) { + GlobalScope.launch { + call.stopRecording() + } + showEndRecordingDialog = false + }, + negativeButton = Triple( + "Cancel", + VideoTheme.styles.buttonStyles.tetriaryButtonStyle(), + ) { + showEndRecordingDialog = false + }, + ) + } } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallStats.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallStats.kt index 8c1af621c6..db8e3e73d2 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallStats.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallStats.kt @@ -72,8 +72,9 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.avatar.UserAvatar +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize import io.getstream.video.android.core.Call import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewCall @@ -229,7 +230,7 @@ fun HeaderWithIconAndBody(icon: ImageVector, header: String, body: String) { style = TextStyle( fontSize = 16.sp, fontWeight = FontWeight(400), - color = VideoTheme.colors.textLowEmphasis, + color = VideoTheme.colors.basePrimary, ), ) } @@ -341,6 +342,7 @@ fun UserAndCallId(call: Call, clipboardManager: ClipboardManager?) { verticalAlignment = Alignment.CenterVertically, ) { UserAvatar( + textSize = StyleSize.S, modifier = Modifier.size(44.dp), userName = call.user.userNameOrId, userImage = call.user.image, @@ -362,7 +364,7 @@ fun UserAndCallId(call: Call, clipboardManager: ClipboardManager?) { fontSize = 16.sp, lineHeight = 16.sp, fontWeight = FontWeight.W400, - color = VideoTheme.colors.textLowEmphasis, + color = VideoTheme.colors.baseSecondary, ), ) } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ChatDialog.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ChatDialog.kt index aa3e0a9de4..e96471b878 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ChatDialog.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ChatDialog.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -43,6 +43,7 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel import io.getstream.chat.android.compose.viewmodel.messages.MessagesViewModelFactory import io.getstream.chat.android.ui.common.state.messages.list.MessageItemState +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.Call import io.getstream.video.android.ui.common.R import java.time.Instant @@ -97,11 +98,12 @@ internal fun ChatDialog( ) } - ChatTheme { + ChatTheme(isInDarkMode = true) { ModalBottomSheetLayout( modifier = Modifier.fillMaxWidth(), sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), sheetState = state, + sheetBackgroundColor = VideoTheme.colors.baseSheetPrimary, sheetContent = { if (state.isVisible) { Column( diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ChatOverly.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ChatOverly.kt index a9e0a559ac..a12341b6fe 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ChatOverly.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ChatOverly.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CustomReactionContent.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CustomReactionContent.kt index 39ed8e4671..aa4e1c0888 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CustomReactionContent.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CustomReactionContent.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.call.renderer.VideoRendererStyle import io.getstream.video.android.core.ParticipantState import io.getstream.video.android.core.model.Reaction @@ -88,7 +88,7 @@ fun BoxScope.CustomReactionContent( modifier = Modifier .padding(top = maxHeight * 0.10f) .align(style.reactionPosition), - fontSize = VideoTheme.dimens.reactionSize.value.sp, + fontSize = VideoTheme.dimens.componentHeightM.value.sp, ) } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/FeedbackDialog.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/FeedbackDialog.kt new file mode 100644 index 0000000000..36155b5bb9 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/FeedbackDialog.kt @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2014-2024 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.call + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ErrorOutline +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.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue +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 io.getstream.video.android.R +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamButton +import io.getstream.video.android.compose.ui.components.base.StreamDialog +import io.getstream.video.android.compose.ui.components.base.StreamDialogPositiveNegative +import io.getstream.video.android.compose.ui.components.base.StreamTextField +import io.getstream.video.android.compose.ui.components.base.styling.ButtonStyles +import io.getstream.video.android.compose.ui.components.base.styling.StreamDialogStyles +import io.getstream.video.android.compose.ui.components.base.styling.StreamTextFieldStyles +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize +import io.getstream.video.android.core.Call +import io.getstream.video.android.mock.StreamPreviewDataUtils +import io.getstream.video.android.mock.previewCall +import io.getstream.video.android.util.FeedbackSender + +@Composable +fun FeedbackDialog(call: Call, onDismiss: () -> Unit) { + var email by remember { mutableStateOf(TextFieldValue("")) } + var message by remember { mutableStateOf(TextFieldValue("")) } + var isError by remember { mutableStateOf(false) } + var feedbackFinished by remember { mutableStateOf(false) } + var feedbackError by remember { mutableStateOf(false) } + val sender = remember { FeedbackSender() } + + if (feedbackFinished) { + StreamDialog(style = VideoTheme.styles.dialogStyles.defaultDialogStyle()) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Image( + painter = painterResource(id = R.drawable.feedback_artwork), + contentDescription = "artwork", + ) + if (feedbackError) { + Spacer(modifier = Modifier.size(8.dp)) + Icon( + imageVector = Icons.Default.ErrorOutline, + contentDescription = "alert", + tint = VideoTheme.colors.alertWarning, + ) + Spacer(modifier = Modifier.size(8.dp)) + } + Text( + text = "Your message was successfully sent", + style = TextStyle( + fontSize = 24.sp, + lineHeight = 28.sp, + fontWeight = FontWeight(500), + color = VideoTheme.colors.basePrimary, + textAlign = TextAlign.Center, + ), + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = + if (feedbackError) { + "Something happened and we could not process your request.\n Please try agian later." + } else { + "Thank you for letting us know how we can continue to improve our\n" + + "product and deliver the best calling experience possible. Hope you had\n" + + "a good call." + }, + style = TextStyle( + fontSize = 16.sp, + lineHeight = 18.5.sp, + fontWeight = FontWeight(400), + color = VideoTheme.colors.baseSecondary, + textAlign = TextAlign.Center, + ), + ) + + Spacer(modifier = Modifier.size(24.dp)) + Box(modifier = Modifier.fillMaxWidth()) { + StreamButton( + modifier = Modifier.align(Alignment.BottomEnd), + text = "Close", + style = VideoTheme.styles.buttonStyles.tetriaryButtonStyle(), + ) { + onDismiss() + } + } + } + } + } else { + StreamDialogPositiveNegative( + onDismiss = onDismiss, + content = { + Image( + painter = painterResource(id = R.drawable.feedback_artwork), + contentDescription = "artwork", + ) + Text( + text = "How is your call Going?", + style = TextStyle( + fontSize = 24.sp, + lineHeight = 28.sp, + fontWeight = FontWeight(500), + color = VideoTheme.colors.basePrimary, + textAlign = TextAlign.Center, + ), + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = "All feedback is celebrated!", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 18.5.sp, + fontWeight = FontWeight(400), + color = VideoTheme.colors.baseSecondary, + textAlign = TextAlign.Center, + ), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamTextField( + value = email, + placeholder = "Email address (required)", + onValueChange = { + email = it + }, + error = isError, + style = StreamTextFieldStyles.defaultTextField(StyleSize.S), + ) + + Spacer(modifier = Modifier.size(16.dp)) + StreamTextField( + value = message, + placeholder = "Message", + onValueChange = { + message = it + }, + minLines = 7, + style = StreamTextFieldStyles.defaultTextField(StyleSize.S), + ) + }, + style = StreamDialogStyles.defaultDialogStyle(), + positiveButton = Triple( + "Submit", + ButtonStyles.secondaryButtonStyle(StyleSize.S), + ) { + if (email.text.isEmpty() || !sender.isValidEmail(email.text)) { + isError = true + } else { + sender.sendFeedback(email.text, message.text, call.cid) { + feedbackError = it + feedbackFinished = true + } + } + }, + negativeButton = Triple( + "Not now", + ButtonStyles.tetriaryButtonStyle(StyleSize.S), + ) { + onDismiss() + }, + ) + } +} + +@Preview +@Composable +private fun FeedbackDialogPreview() { + StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + FeedbackDialog(call = previewCall) { + } + } +} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt new file mode 100644 index 0000000000..3f3f8335d3 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2014-2024 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.call + +import androidx.compose.foundation.background +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CallEnd +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamButton +import io.getstream.video.android.compose.ui.components.call.controls.actions.FlipCameraAction +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.core.Call +import io.getstream.video.android.core.mapper.ReactionMapper +import io.getstream.video.android.mock.StreamPreviewDataUtils +import io.getstream.video.android.mock.previewCall +import io.getstream.video.android.tooling.extensions.toPx + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun LandscapeControls(call: Call, onDismiss: () -> Unit) { + val isCameraEnabled by call.camera.isEnabled.collectAsStateWithLifecycle() + val isMicrophoneEnabled by call.microphone.isEnabled.collectAsStateWithLifecycle() + val toggleCamera = { + call.camera.setEnabled(!isCameraEnabled, true) + } + val toggleMicrophone = { + call.microphone.setEnabled(!isMicrophoneEnabled, true) + } + val onClick = { + call.leave() + } + + Popup( + onDismissRequest = onDismiss, + alignment = Alignment.TopEnd, + offset = IntOffset( + 0, + (VideoTheme.dimens.componentHeightL + VideoTheme.dimens.spacingM).toPx().toInt(), + ), + ) { + LandscapeControlsContent( + isCameraEnabled = isCameraEnabled, + isMicEnabled = isMicrophoneEnabled, + call = call, + camera = toggleCamera, + mic = toggleMicrophone, + onClick = onClick, + ) { + onDismiss() + } + } +} + +@Composable +fun LandscapeControlsContent( + isCameraEnabled: Boolean, + isMicEnabled: Boolean, + call: Call, + camera: () -> Unit, + mic: () -> Unit, + onClick: () -> Unit, + onDismiss: () -> Unit, +) { + Box( + modifier = Modifier + .background( + color = VideoTheme.colors.baseSheetPrimary, + shape = VideoTheme.shapes.dialog, + ) + .width(400.dp), + ) { + Column( + modifier = Modifier + .padding(12.dp), + ) { + ReactionsMenu(call = call, reactionMapper = ReactionMapper.defaultReactionMapper()) { + onDismiss() + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + ToggleCameraAction(isCameraEnabled = isCameraEnabled) { + camera() + } + ToggleMicrophoneAction(isMicrophoneEnabled = isMicEnabled) { + mic() + } + FlipCameraAction { + call.camera.flip() + } + + StreamButton( + style = VideoTheme.styles.buttonStyles.alertButtonStyle(), + icon = Icons.Default.CallEnd, + text = "Leave call", + onClick = onClick, + ) + } + } + } +} + +@Preview +@Composable +fun LandscapeControlsPreview() { + StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LandscapeControlsContent( + true, + false, + previewCall, + {}, + {}, + {}, + ) { + } + } +} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt index 1b018e97ad..99352011dd 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -18,36 +18,30 @@ 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.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AutoAwesome import androidx.compose.material.icons.rounded.AutoAwesome import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import io.getstream.video.android.compose.theme.VideoTheme +import androidx.compose.ui.window.Popup +import io.getstream.video.android.R +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamToggleButton import io.getstream.video.android.compose.ui.components.call.renderer.LayoutType import io.getstream.video.android.mock.StreamPreviewDataUtils +import io.getstream.video.android.tooling.extensions.toPx private data class LayoutChooserDataItem( val which: LayoutType, @@ -72,157 +66,46 @@ internal fun LayoutChooser( 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, + Popup( + offset = IntOffset( + 0, + (VideoTheme.dimens.componentHeightL + VideoTheme.dimens.spacingS).toPx().toInt(), + ), + onDismissRequest = onDismiss, ) { - 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, + Column( + Modifier.background( + color = VideoTheme.colors.baseSheetPrimary, + shape = VideoTheme.shapes.sheet, ) - } - } -} - -@Composable -private fun DynamicRepresentation() { - Column { - Card( - modifier = Modifier - .weight(2f) - .fillMaxWidth() - .padding(2.dp), - backgroundColor = VideoTheme.colors.participantContainerBackground, + .width(300.dp), ) { - } - - 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, - ) { - } + layouts.forEach { layout -> + + val state = ToggleableState(layout.which == current) + val icon = when (layout.which) { + LayoutType.DYNAMIC -> Icons.Default.AutoAwesome + LayoutType.SPOTLIGHT -> ImageVector.vectorResource( + R.drawable.ic_layout_spotlight, + ) + LayoutType.GRID -> ImageVector.vectorResource(R.drawable.ic_layout_grid) } - } - } - } -} - -@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, + StreamToggleButton( + onText = layout.text, + offText = layout.text, + toggleState = rememberUpdatedState(newValue = state), + onIcon = icon, + onStyle = VideoTheme.styles.buttonStyles.toggleButtonStyleOn(), + offStyle = VideoTheme.styles.buttonStyles.toggleButtonStyleOff(), ) { + onLayoutChoice(layout.which) } } } } } -@Preview(showBackground = true) +@Preview @Composable private fun LayoutChooserPreview() { StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) @@ -235,87 +118,28 @@ private fun LayoutChooserPreview() { } } -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview @Composable -private fun LayoutChooserPreviewDark() { +private fun LayoutChooserPreview2() { StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) VideoTheme { LayoutChooser( - current = LayoutType.GRID, + current = LayoutType.SPOTLIGHT, onLayoutChoice = {}, onDismiss = {}, ) } } -@Preview(showBackground = true) -@Composable -private fun GridItemPreview() { - StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) - VideoTheme { - LayoutItem( - current = LayoutType.GRID, - item = layouts[2], - ) - } -} - -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun GridItemPreviewDark() { - StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) - VideoTheme { - LayoutItem( - current = LayoutType.GRID, - item = layouts[2], - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun SpotlightItemPreview() { - StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) - VideoTheme { - LayoutItem( - current = LayoutType.GRID, - item = layouts[1], - ) - } -} - -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview @Composable -private fun SpotlightItemPreviewDark() { +private fun LayoutChooserPreview3() { StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) VideoTheme { - LayoutItem( - current = LayoutType.GRID, - item = layouts[1], - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun DynamicItemPreview() { - StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) - VideoTheme { - LayoutItem( - current = LayoutType.GRID, - item = layouts[0], - ) - } -} - -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun DynamicItemPreviewDark() { - StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) - VideoTheme { - LayoutItem( - current = LayoutType.GRID, - item = layouts[0], + LayoutChooser( + current = LayoutType.DYNAMIC, + onLayoutChoice = {}, + onDismiss = {}, ) } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ParticipantsDialog.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ParticipantsDialog.kt new file mode 100644 index 0000000000..6a091a6ec7 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ParticipantsDialog.kt @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2014-2024 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.call + +import android.content.ClipboardManager +import android.content.Context +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.MicOff +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material.icons.filled.VideocamOff +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.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.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.avatar.UserAvatar +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize +import io.getstream.video.android.core.Call +import io.getstream.video.android.core.ParticipantState +import io.getstream.video.android.mock.StreamPreviewDataUtils +import io.getstream.video.android.mock.previewCall +import io.getstream.video.android.mock.previewParticipantsList +import io.getstream.video.android.util.config.AppConfig + +@Composable +public fun ParticipantsDialog(call: Call, onDismiss: () -> Unit) { + Dialog( + properties = DialogProperties(usePlatformDefaultWidth = false), + onDismissRequest = onDismiss, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + color = Color.Black, + ), + ) { + ParticipantsList(call = call) + IconButton(modifier = Modifier.align(Alignment.TopEnd), onClick = { + onDismiss() + }) { + Icon( + tint = Color.White, + imageVector = Icons.Default.Close, + contentDescription = Icons.Default.Close.name, + ) + } + Spacer(modifier = Modifier.size(VideoTheme.dimens.spacingM)) + } + } +} + +@Composable +fun ParticipantsList(call: Call) { + val participants by call.state.participants.collectAsStateWithLifecycle() + val context = LocalContext.current + val clipboardManager = remember(context) { + context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager + } + ParticipantsListContent(call, clipboardManager, participants) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ParticipantsListContent( + call: Call, + clipboardManager: ClipboardManager? = null, + participants: List, +) { + val context = LocalContext.current + LazyColumn { + item { + Text( + text = "Participants (${participants.size})", + style = VideoTheme.typography.labelM, + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.size(16.dp)) + val env = AppConfig.currentEnvironment.collectAsStateWithLifecycle() + ShareCallWithOthers( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + call, + clipboardManager, + env, + context, + ) + Spacer(modifier = Modifier.size(16.dp)) + } + + items(count = participants.size, key = { index -> participants[index].sessionId }) { + val participant = participants[it] + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = VideoTheme.dimens.spacingM), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + val userName by participant.userNameOrId.collectAsStateWithLifecycle() + val userImage by participant.image.collectAsStateWithLifecycle() + UserAvatar( + textSize = StyleSize.S, + modifier = Modifier.size(VideoTheme.dimens.genericXxl), + userName = userName, + userImage = userImage, + isShowingOnlineIndicator = false, + ) + Spacer(modifier = Modifier.size(VideoTheme.dimens.spacingM)) + Text( + modifier = Modifier + .padding(start = 8.dp), + text = userName, + style = VideoTheme.typography.bodyM, + color = VideoTheme.colors.basePrimary, + fontSize = 16.sp, + maxLines = 1, + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + val audioEnabled by participant.audioEnabled.collectAsStateWithLifecycle() + val iconAudio = if (audioEnabled) { + Icons.Default.Mic + } else { + Icons.Default.MicOff + } + Spacer(modifier = Modifier.width(8.dp)) + Icon( + tint = VideoTheme.colors.basePrimary, + imageVector = iconAudio, + contentDescription = null, + ) + Spacer(modifier = Modifier.width(8.dp)) + + val videoEnabled by participant.videoEnabled.collectAsStateWithLifecycle() + val iconVideo = if (videoEnabled) { + Icons.Default.Videocam + } else { + Icons.Default.VideocamOff + } + Icon( + tint = VideoTheme.colors.basePrimary, + imageVector = iconVideo, + contentDescription = null, + ) + } + } + Spacer(modifier = Modifier.size(VideoTheme.dimens.spacingM)) + } + } +} + +@Preview +@Composable +private fun ParticipantsDialogPreview() { + StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + ParticipantsListContent(call = previewCall, participants = previewParticipantsList) + } +} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ReactionsMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ReactionsMenu.kt index d093ec7e46..f579d9f876 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ReactionsMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ReactionsMenu.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -18,30 +18,22 @@ 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 -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card -import androidx.compose.material.Text +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.requiredWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope 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.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamButton +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize import io.getstream.video.android.core.Call import io.getstream.video.android.core.mapper.ReactionMapper import io.getstream.video.android.mock.StreamPreviewDataUtils @@ -88,78 +80,62 @@ internal fun ReactionsMenu( onDismiss: () -> Unit, ) { val scope = rememberCoroutineScope() - val modifier = Modifier - .background( - color = VideoTheme.colors.barsBackground, - shape = RoundedCornerShape(2.dp), - ) - .wrapContentWidth() val onEmojiSelected: (emoji: String) -> Unit = { sendReaction(scope, call, it, onDismiss) } - - Dialog(onDismiss) { - Card( - modifier = modifier.wrapContentWidth(), - backgroundColor = VideoTheme.colors.barsBackground, + Column(Modifier.fillMaxWidth()) { + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + maxItemsInEachRow = 5, + verticalArrangement = Arrangement.Center, ) { - Column(Modifier.padding(16.dp)) { - Row(horizontalArrangement = Arrangement.Center) { - ReactionItem( - modifier = Modifier - .background( - color = VideoTheme.colors.appBackground, - shape = RoundedCornerShape(2.dp), - ) - .fillMaxWidth(), - textModifier = Modifier.fillMaxWidth(), - reactionMapper = reactionMapper, - reaction = DefaultReactionsMenuData.mainReaction, - onEmojiSelected = onEmojiSelected, - ) - } - FlowRow( - horizontalArrangement = Arrangement.Center, - maxItemsInEachRow = 3, - verticalArrangement = Arrangement.Center, - ) { - DefaultReactionsMenuData.defaultReactions.forEach { - ReactionItem( - modifier = modifier, - reactionMapper = reactionMapper, - onEmojiSelected = onEmojiSelected, - reaction = it, - ) - } - } + DefaultReactionsMenuData.defaultReactions.forEach { + ReactionItem( + reactionMapper = reactionMapper, + onEmojiSelected = onEmojiSelected, + reaction = it, + ) } } + + Row(horizontalArrangement = Arrangement.Center) { + ReactionItem( + showText = true, + reactionMapper = reactionMapper, + reaction = DefaultReactionsMenuData.mainReaction, + onEmojiSelected = onEmojiSelected, + ) + } } } @Composable private fun ReactionItem( - modifier: Modifier = Modifier, - textModifier: Modifier = Modifier, reactionMapper: ReactionMapper, reaction: ReactionItemData, + showText: Boolean = false, onEmojiSelected: (emoji: String) -> Unit, ) { val mappedEmoji = reactionMapper.map(reaction.emojiCode) - Box( - modifier = modifier - .clickable { - onEmojiSelected(reaction.emojiCode) - } - .padding(2.dp), - ) { - Text( - textAlign = TextAlign.Center, - modifier = textModifier.padding(12.dp), - text = "$mappedEmoji ${reaction.displayText}", - color = VideoTheme.colors.textHighEmphasis, - ) + val text = if (showText) { + "$mappedEmoji ${reaction.displayText}" + } else { + mappedEmoji + } + val modifier = if (showText) { + Modifier.fillMaxWidth() + } else { + Modifier + .requiredWidth(VideoTheme.dimens.componentHeightL) + .requiredHeight(VideoTheme.dimens.componentHeightL) } + StreamButton( + modifier = modifier, + style = VideoTheme.styles.buttonStyles.primaryIconButtonStyle(StyleSize.S), + text = text, + onClick = { onEmojiSelected(reaction.emojiCode) }, + ) } private fun sendReaction(scope: CoroutineScope, call: Call, emoji: String, onDismiss: () -> Unit) { @@ -169,26 +145,7 @@ private fun sendReaction(scope: CoroutineScope, call: Call, emoji: String, onDis } } -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ReactionItemPreview() { - StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) - 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 { diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/SettingsMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/SettingsMenu.kt deleted file mode 100644 index 516d996ea6..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/SettingsMenu.kt +++ /dev/null @@ -1,312 +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.call - -import android.app.Activity -import android.graphics.Bitmap -import android.graphics.drawable.Icon -import android.media.projection.MediaProjectionManager -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.DrawableRes -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -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.filled.AutoGraph -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Popup -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.core.Call -import io.getstream.video.android.core.call.audio.AudioFilter -import io.getstream.video.android.core.call.video.BitmapVideoFilter -import io.getstream.video.android.ui.common.R -import io.getstream.video.android.util.BlurredBackgroundVideoFilter -import io.getstream.video.android.util.SampleAudioFilter -import kotlinx.coroutines.launch -import java.nio.ByteBuffer - -@Composable -internal fun SettingsMenu( - call: Call, - showDebugOptions: Boolean, - isBackgroundBlurEnabled: Boolean, - onDisplayAvailableDevice: () -> Unit, - onDismissed: () -> Unit, - onShowReactionsMenu: () -> Unit, - onToggleBackgroundBlur: () -> Unit, - onShowCallStats: () -> Unit, -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - - val screenSharePermissionResult = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult(), - onResult = { - if (it.resultCode == Activity.RESULT_OK && it.data != null) { - call.startScreenSharing(it.data!!) - } - onDismissed.invoke() - }, - ) - - val isScreenSharing by call.screenShare.isEnabled.collectAsStateWithLifecycle() - val screenShareButtonText = if (isScreenSharing) { - "Stop screen-sharing" - } else { - "Start screen-sharing" - } - - Popup( - alignment = Alignment.BottomStart, - offset = IntOffset(30, -210), - onDismissRequest = { onDismissed.invoke() }, - ) { - Card( - shape = RoundedCornerShape(12.dp), - elevation = 6.dp, - ) { - Column( - modifier = Modifier - .width(245.dp) - .background(VideoTheme.colors.appBackground) - .padding(12.dp), - ) { - MenuEntry( - icon = R.drawable.stream_video_ic_reaction, - label = "Reactions", - onClick = { - onDismissed() - onShowReactionsMenu() - }, - ) - - Spacer(modifier = Modifier.height(12.dp)) - - MenuEntry( - imageVector = Icons.Default.AutoGraph, - label = "Call stats", - onClick = { - onDismissed() - onShowCallStats() - }, - ) - - Spacer(modifier = Modifier.height(12.dp)) - - MenuEntry( - icon = R.drawable.stream_video_ic_screensharing, - label = screenShareButtonText, - onClick = { - if (!isScreenSharing) { - scope.launch { - val mediaProjectionManager = context.getSystemService( - MediaProjectionManager::class.java, - ) - screenSharePermissionResult.launch( - mediaProjectionManager.createScreenCaptureIntent(), - ) - } - } else { - call.stopScreenSharing() - } - }, - ) - - Spacer(modifier = Modifier.height(12.dp)) - - MenuEntry( - icon = io.getstream.video.android.R.drawable.ic_mic, - label = "Switch Microphone", - onClick = { - onDismissed.invoke() - onDisplayAvailableDevice.invoke() - }, - ) - - Spacer(modifier = Modifier.height(12.dp)) - - MenuEntry( - icon = if (isBackgroundBlurEnabled) { - io.getstream.video.android.R.drawable.ic_blur_off - } else { - io.getstream.video.android.R.drawable.ic_blur_on - }, - label = if (isBackgroundBlurEnabled) { - "Disable background blur" - } else { - "Enable background blur (beta)" - }, - onClick = { - onToggleBackgroundBlur() - - if (call.videoFilter == null) { - call.videoFilter = object : BitmapVideoFilter() { - val filter = BlurredBackgroundVideoFilter() - - override fun filter(bitmap: Bitmap) { - filter.applyFilter(bitmap) - } - } - } else { - call.videoFilter = null - } - }, - ) - - if (showDebugOptions) { - Spacer(modifier = Modifier.height(12.dp)) - - MenuEntry( - icon = R.drawable.stream_video_ic_fullscreen_exit, - label = "Toggle audio filter", - onClick = { - if (call.audioFilter == null) { - call.audioFilter = object : AudioFilter { - override fun filter( - audioFormat: Int, - channelCount: Int, - sampleRate: Int, - sampleData: ByteBuffer, - ) { - SampleAudioFilter.toRoboticVoice( - sampleData, - channelCount, - 0.8f, - ) - } - } - } else { - call.audioFilter = null - } - }, - ) - - Spacer(modifier = Modifier.height(12.dp)) - - MenuEntry( - icon = R.drawable.stream_video_ic_fullscreen_exit, - label = "Restart Subscriber Ice", - onClick = { - call.debug.restartSubscriberIce() - onDismissed.invoke() - Toast.makeText( - context, - "Restart Subscriber Ice", - Toast.LENGTH_SHORT, - ).show() - }, - ) - - Spacer(modifier = Modifier.height(12.dp)) - - MenuEntry( - icon = R.drawable.stream_video_ic_fullscreen_exit, - label = "Restart Publisher Ice", - onClick = { - call.debug.restartPublisherIce() - onDismissed.invoke() - Toast.makeText( - context, - "Restart Publisher Ice", - Toast.LENGTH_SHORT, - ).show() - }, - ) - - Spacer(modifier = Modifier.height(12.dp)) - - MenuEntry( - icon = R.drawable.stream_video_ic_fullscreen_exit, - label = "Kill SFU WS", - onClick = { - call.debug.doFullReconnection() - onDismissed.invoke() - Toast.makeText( - context, - "Killing SFU WS. Should trigger reconnect...", - Toast.LENGTH_SHORT, - ).show() - }, - ) - - Spacer(modifier = Modifier.height(12.dp)) - - MenuEntry( - icon = R.drawable.stream_video_ic_fullscreen, - label = "Switch sfu", - onClick = { - call.debug.switchSfu() - onDismissed.invoke() - Toast.makeText(context, "Switch sfu", Toast.LENGTH_SHORT).show() - }, - ) - } - } - } - } -} - -@Composable -private fun MenuEntry( - @DrawableRes icon: Int? = null, - imageVector: ImageVector? = null, - label: String, - onClick: () -> Unit, -) { - Row(modifier = Modifier.clickable(onClick = onClick)) { - imageVector?.let { - Icon( - imageVector = imageVector, - tint = VideoTheme.colors.textHighEmphasis, - contentDescription = null, - ) - } - icon?.let { - Icon( - painter = painterResource(id = icon), - tint = VideoTheme.colors.textHighEmphasis, - contentDescription = null, - ) - } - Text( - modifier = Modifier.padding(start = 12.dp, top = 2.dp), - text = label, - color = VideoTheme.colors.textHighEmphasis, - ) - } -} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ShareCall.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ShareCall.kt new file mode 100644 index 0000000000..4bada6e585 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ShareCall.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2014-2024 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.call + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import androidx.compose.foundation.background +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CopyAll +import androidx.compose.material.icons.filled.PersonAddAlt1 +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamButton +import io.getstream.video.android.core.Call +import io.getstream.video.android.util.config.types.StreamEnvironment + +@Composable +public fun ShareCallWithOthers( + modifier: Modifier = Modifier, + call: Call, + clipboardManager: ClipboardManager?, + env: State, + context: Context, +) { + ShareSettingsBox(modifier, call, clipboardManager) { + val link = "${env.value?.sharelink}${call.id}" + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, link) + type = "text/plain" + } + val shareIntent = Intent.createChooser(sendIntent, null) + context.startActivity(shareIntent) + } +} + +@Composable +public fun ShareSettingsBox( + modifier: Modifier = Modifier, + call: Call, + clipboardManager: ClipboardManager?, + onShare: (String) -> Unit, +) { + Box( + modifier = modifier + .background( + color = VideoTheme.colors.baseSheetTertiary, + shape = VideoTheme.shapes.dialog, + ), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(VideoTheme.dimens.spacingL), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + StreamButton( + modifier = Modifier.fillMaxWidth(), + text = "Share link with others", + icon = Icons.Default.PersonAddAlt1, + style = VideoTheme.styles.buttonStyles.secondaryButtonStyle(), + ) { + onShare(call.id) + } + Spacer(modifier = Modifier.size(16.dp)) + Text( + text = "Or share this call ID with the \u2028others you want in the meeting", + style = VideoTheme.typography.bodyM, + ) + Spacer(modifier = Modifier.size(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row { + Text( + text = "Call ID: ", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 20.sp, + fontWeight = FontWeight(500), + color = Color.White, + ), + ) + Text( + modifier = Modifier.width(200.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + text = call.id, + softWrap = false, + style = TextStyle( + fontSize = 16.sp, + lineHeight = 16.sp, + fontWeight = FontWeight.W400, + color = VideoTheme.colors.brandCyan, + ), + ) + } + + Spacer(modifier = Modifier.size(8.dp)) + IconButton( + modifier = Modifier.size(32.dp), + onClick = { + val clipData = ClipData.newPlainText("Call ID", call.id) + clipboardManager?.setPrimaryClip(clipData) + }, + ) { + Icon( + tint = Color.White, + imageVector = Icons.Default.CopyAll, + contentDescription = "Copy", + ) + } + } + Spacer(modifier = Modifier.size(16.dp)) + } + } +} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt index e681918b70..9286cf130d 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -18,11 +18,10 @@ package io.getstream.video.android.ui.join -import androidx.compose.foundation.BorderStroke +import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -39,29 +38,31 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Icon -import androidx.compose.material.IconButton import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.TextField -import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Call +import androidx.compose.material.icons.filled.Login +import androidx.compose.material.icons.filled.Logout +import androidx.compose.material.icons.filled.QrCodeScanner +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.VideoCall import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.boundsInParent +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag @@ -69,27 +70,40 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInOptions import io.getstream.video.android.BuildConfig import io.getstream.video.android.R -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.avatar.UserAvatar -import io.getstream.video.android.datastore.delegate.StreamUserDataStore +import io.getstream.video.android.compose.ui.components.base.StreamButton +import io.getstream.video.android.compose.ui.components.base.StreamDialogPositiveNegative +import io.getstream.video.android.compose.ui.components.base.StreamIconToggleButton +import io.getstream.video.android.compose.ui.components.base.StreamTextField +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewUsers import io.getstream.video.android.model.User import io.getstream.video.android.tooling.util.StreamFlavors -import io.getstream.video.android.ui.theme.Colors -import io.getstream.video.android.ui.theme.StreamButton -import io.getstream.video.android.util.NetworkMonitor +import io.getstream.video.android.util.LockScreenOrientation +import io.getstream.video.android.util.config.AppConfig +import io.getstream.video.android.util.config.types.StreamEnvironment @Composable fun CallJoinScreen( @@ -99,6 +113,7 @@ fun CallJoinScreen( navigateToDirectCallJoin: () -> Unit, navigateToBarcodeScanner: () -> Unit = {}, ) { + LockScreenOrientation(orientation = Configuration.ORIENTATION_PORTRAIT) val uiState by callJoinViewModel.uiState.collectAsState(CallJoinUiState.Nothing) val user by callJoinViewModel.user.collectAsState(initial = null) @@ -115,7 +130,8 @@ fun CallJoinScreen( Column( modifier = Modifier .fillMaxSize() - .background(Colors.background), + .background(VideoTheme.colors.baseSheetPrimary), + verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally, ) { CallJoinHeader( @@ -127,11 +143,10 @@ fun CallJoinScreen( callJoinViewModel.logOut() }, ) - + Spacer(modifier = Modifier.size(VideoTheme.dimens.genericMax)) CallJoinBody( modifier = Modifier .align(Alignment.CenterHorizontally) - .widthIn(0.dp, 500.dp) .verticalScroll(rememberScrollState()) .weight(1f), callJoinViewModel = callJoinViewModel, @@ -168,11 +183,9 @@ private fun HandleCallJoinUiState( ) { LaunchedEffect(key1 = callJoinUiState) { when (callJoinUiState) { - is CallJoinUiState.JoinCompleted -> - navigateToCallLobby.invoke(callJoinUiState.callId) + is CallJoinUiState.JoinCompleted -> navigateToCallLobby.invoke(callJoinUiState.callId) - is CallJoinUiState.GoBackToLogin -> - navigateUpToLogin.invoke() + is CallJoinUiState.GoBackToLogin -> navigateUpToLogin.invoke() else -> Unit } @@ -183,19 +196,22 @@ private fun HandleCallJoinUiState( @Composable private fun CallJoinHeader( user: User?, + isProduction: Boolean = BuildConfig.FLAVOR == StreamFlavors.production, + showDirectCall: Boolean = user?.custom?.get("email")?.contains("getstreamio") == true, onAvatarLongClick: () -> Unit, onDirectCallClick: () -> Unit, onSignOutClick: () -> Unit, ) { Row( modifier = Modifier - .fillMaxWidth() - .padding(24.dp), + .padding(VideoTheme.dimens.spacingM) + .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround, ) { user?.let { Box( - modifier = if (BuildConfig.FLAVOR == StreamFlavors.production) { + modifier = if (isProduction) { Modifier.combinedClickable( interactionSource = remember { MutableInteractionSource() }, indication = null, @@ -207,7 +223,8 @@ private fun CallJoinHeader( }, ) { UserAvatar( - modifier = Modifier.size(24.dp), + modifier = Modifier.size(VideoTheme.dimens.componentHeightL), + textSize = StyleSize.S, userName = it.userNameOrId, userImage = it.image, ) @@ -224,22 +241,78 @@ private fun CallJoinHeader( fontSize = 16.sp, ) - 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() }, - ) - } - - if (BuildConfig.FLAVOR == StreamFlavors.development) { - Spacer(modifier = Modifier.width(5.dp)) + if (!isProduction || showDirectCall) { + var showMenu by remember { + mutableStateOf(false) + } + var popupPosition by remember { mutableStateOf(IntOffset(0, 0)) } + var buttonSize by remember { mutableStateOf(IntSize(0, 0)) } + + StreamIconToggleButton( + modifier = Modifier.onGloballyPositioned { coordinates -> + val buttonBounds = coordinates.boundsInParent() + popupPosition = IntOffset( + x = buttonBounds.right.toInt() - buttonSize.width, + y = buttonBounds.bottom.toInt(), + ) + buttonSize = coordinates.size + }, + toggleState = rememberUpdatedState(newValue = ToggleableState(showMenu)), + onIcon = Icons.Default.Settings, + onStyle = VideoTheme.styles.buttonStyles.secondaryIconButtonStyle(), + offStyle = VideoTheme.styles.buttonStyles.primaryIconButtonStyle(), + ) { + showMenu = when (it) { + ToggleableState.On -> false + ToggleableState.Off -> true + ToggleableState.Indeterminate -> false + } + } - StreamButton( - modifier = Modifier.widthIn(125.dp), - text = stringResource(id = R.string.sign_out), - onClick = onSignOutClick, - ) + if (showMenu) { + Popup( + onDismissRequest = { + showMenu = !showMenu + }, + offset = popupPosition, + ) { + Column( + modifier = Modifier + .width(200.dp) + .background( + VideoTheme.colors.baseSheetTertiary, + VideoTheme.shapes.dialog, + ) + .padding(VideoTheme.dimens.spacingM), + ) { + if (showDirectCall) { + StreamButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.direct_call), + icon = Icons.Default.Call, + style = VideoTheme.styles.buttonStyles.primaryButtonStyle(), + onClick = { + showMenu = false + onDirectCallClick.invoke() + }, + ) + } + Spacer(modifier = Modifier.width(5.dp)) + if (!isProduction) { + StreamButton( + modifier = Modifier.fillMaxWidth(), + icon = Icons.Default.Logout, + style = VideoTheme.styles.buttonStyles.tetriaryButtonStyle(), + text = stringResource(id = R.string.sign_out), + onClick = { + showMenu = false + onSignOutClick() + }, + ) + } + } + } + } } } } @@ -251,6 +324,7 @@ private fun CallJoinBody( callJoinViewModel: CallJoinViewModel = hiltViewModel(), isNetworkAvailable: Boolean, ) { + val selectedEnv by AppConfig.currentEnvironment.collectAsStateWithLifecycle() val user by if (LocalInspectionMode.current) { remember { mutableStateOf(previewUsers[0]) } } else { @@ -264,60 +338,66 @@ private fun CallJoinBody( verticalArrangement = Arrangement.Center, ) { StreamLogo() - Spacer(modifier = Modifier.height(25.dp)) - - AppName() - + AppName(selectedEnv) Spacer(modifier = Modifier.height(25.dp)) - Description(text = stringResource(id = R.string.you_are_offline)) } } else { - Column( - modifier = modifier - .fillMaxSize() - .background(Colors.background) - .semantics { testTagsAsResourceId = true }, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (user != null) { - StreamLogo() - - Spacer(modifier = Modifier.height(25.dp)) - - AppName() - - Spacer(modifier = Modifier.height(20.dp)) - - Description(text = stringResource(id = R.string.join_description)) - - Spacer(modifier = Modifier.height(42.dp)) - - Label(text = stringResource(id = R.string.call_id_number)) - - Spacer(modifier = Modifier.height(8.dp)) - - JoinCallForm(openCamera = openCamera, callJoinViewModel = callJoinViewModel) - - Spacer(modifier = Modifier.height(25.dp)) - - Label(text = stringResource(id = R.string.join_call_no_id_hint)) - - Spacer(modifier = Modifier.height(8.dp)) + if (user != null) { + CallActualContent(modifier = modifier.fillMaxSize(), onJoinCall = { + callJoinViewModel.handleUiEvent(CallJoinEvent.JoinCall(callId = it)) + }, onNewCall = { + callJoinViewModel.handleUiEvent(CallJoinEvent.JoinCall()) + }, gotoQR = { + openCamera() + }) + } + } +} - StreamButton( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .padding(horizontal = 35.dp) - .testTag("start_new_call"), - text = stringResource(id = R.string.start_a_new_call), - onClick = { callJoinViewModel.handleUiEvent(CallJoinEvent.JoinCall()) }, - ) - } +@Composable +private fun CallActualContent( + modifier: Modifier = Modifier, + onJoinCall: (String) -> Unit, + onNewCall: () -> Unit, + gotoQR: () -> Unit, +) = Box(modifier = Modifier.background(VideoTheme.colors.baseSheetPrimary)) { + Column( + modifier = modifier + .padding(horizontal = VideoTheme.dimens.spacingM) + .semantics { testTagsAsResourceId = true }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + StreamLogo() + Spacer(modifier = Modifier.height(VideoTheme.dimens.spacingL)) + AppName() + Spacer(modifier = Modifier.height(20.dp)) + Description(text = stringResource(id = R.string.join_description)) + Spacer(modifier = Modifier.height(VideoTheme.dimens.spacingL)) + JoinCallForm { + onJoinCall(it) } + Spacer(modifier = Modifier.height(VideoTheme.dimens.spacingS)) + StreamButton( + modifier = Modifier + .fillMaxWidth() + .testTag("start_new_call"), + text = stringResource(id = R.string.start_a_new_call), + icon = Icons.Default.VideoCall, + onClick = { onNewCall() }, + ) + Spacer(modifier = Modifier.height(VideoTheme.dimens.spacingS)) + StreamButton( + style = VideoTheme.styles.buttonStyles.tetriaryButtonStyle(), + modifier = Modifier + .fillMaxWidth() + .testTag("scan_qr_code"), + text = stringResource(id = R.string.scan_qr_code), + icon = Icons.Default.QrCodeScanner, + onClick = { gotoQR() }, + ) } } @@ -331,13 +411,22 @@ private fun StreamLogo() { } @Composable -private fun AppName() { +private fun AppName(env: StreamEnvironment? = null) { Text( - modifier = Modifier.padding(horizontal = 30.dp), - text = stringResource(id = R.string.app_name), - color = Color.White, - fontSize = 32.sp, + modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, + text = buildAnnotatedString { + append("Stream\n") + append( + AnnotatedString( + "[Video Calling]\n", + spanStyle = SpanStyle(VideoTheme.colors.brandGreen), + ), + ) + append(env?.displayName ?: "") + }, + color = Color.White, + fontSize = 24.sp, ) } @@ -345,9 +434,8 @@ private fun AppName() { private fun Description(text: String) { Text( text = text, - color = Colors.description, + style = VideoTheme.typography.bodyM, textAlign = TextAlign.Center, - fontSize = 18.sp, modifier = Modifier.widthIn(0.dp, 320.dp), ) } @@ -366,82 +454,51 @@ private fun Label(text: String) { @Composable private fun JoinCallForm( - openCamera: () -> Unit, - callJoinViewModel: CallJoinViewModel, + joinCall: (String) -> Unit, ) { var callId by remember { mutableStateOf( - if (BuildConfig.FLAVOR == StreamFlavors.development) { - "default:79cYh3J5JgGk" - } else { - "" - }, + TextFieldValue( + if (BuildConfig.FLAVOR == StreamFlavors.development) { + "default:79cYh3J5JgGk" + } else { + "" + }, + ), ) } Row( modifier = Modifier .fillMaxWidth() - .height(50.dp) - .padding(horizontal = 35.dp), + .height(50.dp), ) { - TextField( + StreamTextField( modifier = Modifier .weight(1f) - .fillMaxHeight() - .border( - BorderStroke(1.dp, Color(0xFF4C525C)), - RoundedCornerShape(6.dp), - ), - shape = RoundedCornerShape(6.dp), - value = callId, - singleLine = true, + .fillMaxHeight(), onValueChange = { callId = it }, - trailingIcon = { - IconButton( - onClick = openCamera, - modifier = Modifier.fillMaxHeight(), - content = { - Icon( - painter = painterResource(id = R.drawable.ic_scan_qr), - contentDescription = stringResource( - id = R.string.join_call_by_qr_code, - ), - tint = Colors.description, - modifier = Modifier.size(36.dp), - ) - }, - ) - }, - colors = TextFieldDefaults.textFieldColors( - textColor = Color.White, - focusedLabelColor = VideoTheme.colors.primaryAccent, - unfocusedIndicatorColor = Colors.secondBackground, - focusedIndicatorColor = Colors.secondBackground, - backgroundColor = Colors.secondBackground, - ), keyboardOptions = KeyboardOptions.Default.copy( keyboardType = KeyboardType.Email, ), - placeholder = { - Text( - stringResource(id = R.string.join_call_call_id_hint), - color = Color(0xFF5D6168), - ) - }, + style = VideoTheme.styles.textFieldStyles.defaultTextField(), + value = callId, + placeholder = stringResource(id = R.string.join_call_call_id_hint), keyboardActions = KeyboardActions( onDone = { - callJoinViewModel.handleUiEvent(CallJoinEvent.JoinCall(callId = callId)) + joinCall(callId.text) }, ), ) StreamButton( + icon = Icons.Default.Login, + style = VideoTheme.styles.buttonStyles.secondaryButtonStyle(), modifier = Modifier .padding(start = 16.dp) .fillMaxHeight() .testTag("join_call"), onClick = { - callJoinViewModel.handleUiEvent(CallJoinEvent.JoinCall(callId = callId)) + joinCall(callId.text) }, text = stringResource(id = R.string.join_call), ) @@ -453,54 +510,63 @@ private fun SignOutDialog( onConfirmation: () -> Unit, onDismissRequest: () -> Unit, ) { - AlertDialog( - modifier = Modifier.border( - BorderStroke(1.dp, Colors.background), - RoundedCornerShape(6.dp), - ), - title = { Text(text = stringResource(id = R.string.sign_out)) }, - text = { Text(text = stringResource(R.string.are_you_sure_sign_out)) }, - confirmButton = { - TextButton(onClick = { onConfirmation() }) { - Text( - text = stringResource(id = R.string.sign_out), - color = VideoTheme.colors.primaryAccent, - ) - } + StreamDialogPositiveNegative( + style = VideoTheme.styles.dialogStyles.defaultDialogStyle(), + positiveButton = Triple( + stringResource(id = R.string.sign_out), + VideoTheme.styles.buttonStyles.secondaryButtonStyle(), + ) { + onConfirmation() }, - dismissButton = { - TextButton(onClick = { onDismissRequest() }) { - Text( - text = stringResource(R.string.cancel), - color = VideoTheme.colors.primaryAccent, - ) - } + negativeButton = Triple( + stringResource(R.string.cancel), + VideoTheme.styles.buttonStyles.tetriaryButtonStyle(), + ) { + onDismissRequest() }, - onDismissRequest = { onDismissRequest }, - shape = RoundedCornerShape(6.dp), - backgroundColor = Colors.secondBackground, - contentColor = Color.White, + title = stringResource(id = R.string.sign_out), + contentText = stringResource(R.string.are_you_sure_sign_out), ) } +class BelowElementPositionProvider( + private val anchorBounds: androidx.compose.ui.geometry.Rect, + private val screenPadding: Int = 8, // Padding from screen edges +) : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset { + val x = anchorBounds.left.coerceIn( + screenPadding, + (windowSize.width - popupContentSize.width - screenPadding), + ) + + val y = (this.anchorBounds.bottom + screenPadding).coerceIn( + screenPadding.toFloat(), + (windowSize.height - popupContentSize.height - screenPadding).toFloat(), + ).toInt() + + return IntOffset(x, y) + } +} + @Preview @Composable private fun CallJoinScreenPreview() { StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) VideoTheme { - StreamUserDataStore.install(LocalContext.current) - CallJoinScreen( - callJoinViewModel = CallJoinViewModel( - dataStore = StreamUserDataStore.instance(), - googleSignInClient = GoogleSignIn.getClient( - LocalContext.current, - GoogleSignInOptions.Builder().build(), - ), - networkMonitor = NetworkMonitor(LocalContext.current), - ), - navigateToCallLobby = {}, - navigateUpToLogin = {}, - navigateToDirectCallJoin = {}, - ) + CallActualContent(onJoinCall = {}, onNewCall = {}, gotoQR = {}) + } +} + +@Preview +@Composable +private fun CallJoinScreenHeader() { + StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + CallJoinHeader(previewUsers[0], false, true, {}, {}, {}) } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinViewModel.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinViewModel.kt index 0f46af359d..b7832937d9 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinViewModel.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/barcode/BardcodeScanner.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/barcode/BardcodeScanner.kt index b694a80734..2f8fcd9934 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/barcode/BardcodeScanner.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/join/barcode/BardcodeScanner.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -39,7 +39,6 @@ import androidx.compose.material.IconButton import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cancel -import androidx.compose.material.icons.outlined.Cancel import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -70,8 +69,8 @@ import com.google.mlkit.vision.common.InputImage import io.getstream.video.android.DeeplinkingActivity import io.getstream.video.android.R import io.getstream.video.android.analytics.FirebaseEvents -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.ui.theme.StreamButton +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamButton import java.util.concurrent.Executor import java.util.concurrent.Executors @@ -99,7 +98,7 @@ internal fun BarcodeScanner(navigateBack: () -> Unit = {}) { when (val cameraPermissionStatus = cameraPermissionState.status) { PermissionStatus.Granted -> { - val color = VideoTheme.colors.primaryAccent + val color = VideoTheme.colors.brandPrimary Box(modifier = Modifier.fillMaxSize()) { CameraPreview(imageAnalysis = imageAnalysis) CornerRectWithArcs(color = color, cornerRadius = 32f, strokeWidth = 12f) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt index a21bc50d4f..988f620b96 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package io.getstream.video.android.ui.lobby import android.content.Intent +import android.content.res.Configuration import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -28,15 +29,21 @@ 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.shape.RoundedCornerShape +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Language +import androidx.compose.material.icons.filled.LockPerson import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -44,55 +51,59 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow 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.SavedStateHandle -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.getstream.video.android.BuildConfig import io.getstream.video.android.R -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.avatar.UserAvatar +import io.getstream.video.android.compose.ui.components.base.StreamButton +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize import io.getstream.video.android.compose.ui.components.call.lobby.CallLobby +import io.getstream.video.android.core.Call import io.getstream.video.android.core.call.state.ToggleCamera import io.getstream.video.android.core.call.state.ToggleMicrophone -import io.getstream.video.android.datastore.delegate.StreamUserDataStore import io.getstream.video.android.mock.StreamPreviewDataUtils -import io.getstream.video.android.tooling.util.StreamFlavors +import io.getstream.video.android.mock.previewCall +import io.getstream.video.android.mock.previewUsers +import io.getstream.video.android.model.User import io.getstream.video.android.ui.call.CallActivity -import io.getstream.video.android.ui.theme.Colors -import io.getstream.video.android.ui.theme.StreamButton +import io.getstream.video.android.util.LockScreenOrientation import kotlinx.coroutines.delay @Composable fun CallLobbyScreen( callLobbyViewModel: CallLobbyViewModel = hiltViewModel(), - navigateUpToLogin: () -> Unit, + onBack: () -> Unit, ) { + LockScreenOrientation(orientation = Configuration.ORIENTATION_PORTRAIT) val isLoading by callLobbyViewModel.isLoading.collectAsState() + val isMicrophoneEnabled by callLobbyViewModel.microphoneEnabled.collectAsStateWithLifecycle() + val isCameraEnabled by callLobbyViewModel.cameraEnabled.collectAsStateWithLifecycle() + val call by remember { + mutableStateOf(callLobbyViewModel.call) + } Box(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier .fillMaxSize() - .background(Colors.background) + .background(VideoTheme.colors.baseSheetPrimary) .testTag("call_lobby"), horizontalAlignment = Alignment.CenterHorizontally, ) { CallLobbyHeader( - navigateUpToLogin = navigateUpToLogin, + onBack = onBack, callLobbyViewModel = callLobbyViewModel, ) @@ -101,14 +112,24 @@ fun CallLobbyScreen( .align(Alignment.CenterHorizontally) .fillMaxWidth() .weight(1f), - callLobbyViewModel = callLobbyViewModel, - ) + isMicrophoneEnabled = isMicrophoneEnabled, + isCameraEnabled = isCameraEnabled, + onToggleCamera = { + callLobbyViewModel.enableCamera(it) + }, + onToggleMicrophone = { + callLobbyViewModel.enableMicrophone(it) + }, + call = call, + ) { + LobbyDescription(callLobbyViewModel = callLobbyViewModel) + } } if (isLoading) { CircularProgressIndicator( modifier = Modifier.align(Alignment.Center), - color = VideoTheme.colors.primaryAccent, + color = VideoTheme.colors.brandPrimary, ) } } @@ -117,7 +138,7 @@ fun CallLobbyScreen( @Composable private fun CallLobbyHeader( callLobbyViewModel: CallLobbyViewModel = hiltViewModel(), - navigateUpToLogin: () -> Unit, + onBack: () -> Unit, ) { val uiState by callLobbyViewModel.uiState.collectAsState(initial = CallLobbyUiState.Nothing) val isLoggedOut by callLobbyViewModel.isLoggedOut.collectAsState(initial = false) @@ -128,15 +149,31 @@ private fun CallLobbyHeader( callLobbyViewModel = callLobbyViewModel, ) + CallLobbyHeaderContent(user, onBack) + + LaunchedEffect(key1 = isLoggedOut) { + if (isLoggedOut) { + onBack.invoke() + } + } +} + +@Composable +private fun CallLobbyHeaderContent( + user: State, + onBack: () -> Unit, +) { Row( modifier = Modifier - .fillMaxWidth() - .padding(24.dp), + .padding(VideoTheme.dimens.spacingM) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { val userValue = user.value if (userValue != null) { UserAvatar( + textSize = StyleSize.S, modifier = Modifier.size(32.dp), userName = userValue.userNameOrId, userImage = userValue.image, @@ -153,96 +190,80 @@ private fun CallLobbyHeader( maxLines = 1, fontSize = 16.sp, ) - - if (BuildConfig.FLAVOR == StreamFlavors.development) { - Spacer(modifier = Modifier.width(4.dp)) - - StreamButton( - modifier = Modifier.width(125.dp), - text = stringResource(id = R.string.sign_out), - onClick = { callLobbyViewModel.signOut() }, + IconButton( + modifier = Modifier + .padding(8.dp), + onClick = { + onBack() + }, + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = null, + tint = VideoTheme.colors.basePrimary, ) } } - - LaunchedEffect(key1 = isLoggedOut) { - if (isLoggedOut) { - navigateUpToLogin.invoke() - } - } } +@OptIn(ExperimentalComposeUiApi::class) @Composable private fun CallLobbyBody( - modifier: Modifier, - callLobbyViewModel: CallLobbyViewModel = hiltViewModel(), + modifier: Modifier = Modifier, + call: Call, + isCameraEnabled: Boolean, + isMicrophoneEnabled: Boolean, + onToggleCamera: (Boolean) -> Unit, + onToggleMicrophone: (Boolean) -> Unit, + description: @Composable () -> Unit, ) { - val call by remember { mutableStateOf(callLobbyViewModel.call) } - Column( modifier = modifier .fillMaxSize() - .background(Colors.background) + .background(VideoTheme.colors.baseSheetPrimary) .semantics { testTagsAsResourceId = true }, verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { - Text( - modifier = Modifier.padding(horizontal = 30.dp), - text = stringResource(id = R.string.stream_video), - color = Color.White, - fontSize = 26.sp, - textAlign = TextAlign.Center, - ) + // Text and Spacer elements remain unchanged - Spacer(modifier = Modifier.height(4.dp)) + // LaunchedEffect to handle initial setup might need adjustments + // based on how you handle benchmarks or initial setup externally + Icon( + modifier = Modifier.size(36.dp), + imageVector = Icons.Default.Language, + tint = VideoTheme.colors.brandGreen, + contentDescription = "", + ) Text( - modifier = Modifier.padding(horizontal = 30.dp), - text = stringResource(id = R.string.call_lobby_description), - color = Colors.description, - textAlign = TextAlign.Center, - fontSize = 17.sp, + modifier = Modifier.padding(VideoTheme.dimens.spacingM), + text = "Set up your test call", + style = VideoTheme.typography.titleS, ) - - Spacer(modifier = Modifier.height(20.dp)) - - val isCameraEnabled: Boolean by if (LocalInspectionMode.current) { - remember { mutableStateOf(true) } - } else { - callLobbyViewModel.cameraEnabled.collectAsState(initial = false) - } - - val isMicrophoneEnabled by if (LocalInspectionMode.current) { - remember { mutableStateOf(true) } - } else { - callLobbyViewModel.microphoneEnabled.collectAsState(initial = false) - } - - // turn on camera and microphone by default - LaunchedEffect(key1 = Unit) { - delay(300) - if (BuildConfig.BUILD_TYPE == "benchmark") { - callLobbyViewModel.call.camera.disable() - callLobbyViewModel.call.microphone.disable() - } - } - CallLobby( call = call, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(VideoTheme.dimens.spacingM), isCameraEnabled = isCameraEnabled, isMicrophoneEnabled = isMicrophoneEnabled, onCallAction = { action -> when (action) { - is ToggleCamera -> callLobbyViewModel.enableCamera(action.isEnabled) - is ToggleMicrophone -> callLobbyViewModel.enableMicrophone(action.isEnabled) + is ToggleCamera -> onToggleCamera(action.isEnabled) + is ToggleMicrophone -> onToggleMicrophone(action.isEnabled) else -> Unit } }, ) - - LobbyDescription(callLobbyViewModel = callLobbyViewModel) + if (BuildConfig.BUILD_TYPE == "benchmark") { + LaunchedEffect(key1 = Unit) { + delay(300) + onToggleCamera(true) + onToggleMicrophone(true) + } + } + description() } } @@ -251,36 +272,61 @@ private fun LobbyDescription( callLobbyViewModel: CallLobbyViewModel, ) { val session by callLobbyViewModel.call.state.session.collectAsState() + val participantsSize = session?.participants?.size ?: 0 - Column( - modifier = Modifier - .padding(horizontal = 35.dp) - .background( - color = VideoTheme.colors.callLobbyBackground, - shape = RoundedCornerShape(16.dp), - ), - ) { - Text( - modifier = Modifier.padding(start = 32.dp, end = 32.dp, top = 12.dp, bottom = 8.dp), - text = stringResource( + LobbyDescriptionContent(participantsSize = participantsSize) { + callLobbyViewModel.handleUiEvent( + CallLobbyEvent.JoinCall, + ) + } +} + +@Composable +private fun LobbyDescriptionContent(participantsSize: Int, onClick: () -> Unit) { + val text = if (participantsSize > 0) { + Pair( + stringResource( id = R.string.join_call_description, - session?.participants?.size ?: 0, + participantsSize, ), - color = Color.White, + stringResource(id = R.string.join_call), + ) + } else { + Pair( + "Start a private test call. This demo is\nbuilt on Stream’s SDKs and runs on our \nglobal edge network.", + "Start a test call", ) + } + Column( + modifier = Modifier.padding(VideoTheme.dimens.spacingM), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = Modifier.wrapContentWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.LockPerson, + tint = VideoTheme.colors.basePrimary, + contentDescription = "", + ) + Text( + modifier = Modifier.padding(horizontal = VideoTheme.dimens.spacingM), + text = text.first, + style = VideoTheme.typography.bodyS, + ) + } + Spacer(modifier = Modifier.size(VideoTheme.dimens.spacingM)) StreamButton( + style = VideoTheme.styles.buttonStyles.secondaryButtonStyle(), modifier = Modifier .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp) - .clip(RoundedCornerShape(12.dp)) .testTag("start_call"), - text = stringResource(id = R.string.join_call), - onClick = { - callLobbyViewModel.handleUiEvent( - CallLobbyEvent.JoinCall, - ) - }, + text = text.second, + onClick = onClick, ) } } @@ -314,21 +360,32 @@ private fun HandleCallLobbyUiState( @Preview @Composable -private fun CallLobbyScreenPreview() { +private fun CallLobbyHeaderPreview() { StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) - StreamUserDataStore.install(LocalContext.current) VideoTheme { - CallLobbyScreen( - callLobbyViewModel = CallLobbyViewModel( - savedStateHandle = SavedStateHandle( - mapOf("cid" to "default:123"), - ), - dataStore = StreamUserDataStore.instance(), - googleSignInClient = GoogleSignIn.getClient( - LocalContext.current, - GoogleSignInOptions.Builder().build(), - ), - ), - ) {} + CallLobbyHeaderContent( + user = remember { + mutableStateOf(previewUsers[0]) + }, + ) { + } + } +} + +@Preview +@Composable +private fun CallLobbyBodyPreview() { + StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + CallLobbyBody( + isCameraEnabled = false, + isMicrophoneEnabled = false, + call = previewCall, + onToggleMicrophone = {}, + onToggleCamera = {}, + ) { + LobbyDescriptionContent(participantsSize = 0) { + } + } } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyViewModel.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyViewModel.kt index a9e804e270..cd2b5ecdee 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyViewModel.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignIn.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignIn.kt index f878a6a3ef..1af22ef21f 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignIn.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignIn.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignInLauncher.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignInLauncher.kt index 7cc172555e..ad5dc5345c 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignInLauncher.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/GoogleSignInLauncher.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt index f6cea72c52..aa380cef57 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -18,8 +18,7 @@ package io.getstream.video.android.ui.login -import android.content.Intent -import android.net.Uri +import android.content.res.Configuration import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -27,7 +26,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -37,47 +35,61 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.AlertDialog import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Surface import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.outlined.GroupAdd import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.Popup import androidx.core.content.ContextCompat.getString -import androidx.core.content.ContextCompat.startActivity import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.getstream.video.android.BuildConfig import io.getstream.video.android.R -import io.getstream.video.android.compose.theme.VideoTheme -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.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamButton +import io.getstream.video.android.compose.ui.components.base.StreamDialogPositiveNegative +import io.getstream.video.android.compose.ui.components.base.StreamIconToggleButton +import io.getstream.video.android.compose.ui.components.base.StreamTextField +import io.getstream.video.android.compose.ui.components.base.styling.ButtonStyles +import io.getstream.video.android.compose.ui.components.base.styling.IconStyles +import io.getstream.video.android.compose.ui.components.base.styling.StreamDialogStyles +import io.getstream.video.android.tooling.extensions.toPx +import io.getstream.video.android.util.LockScreenOrientation import io.getstream.video.android.util.UserHelper import io.getstream.video.android.util.config.AppConfig import io.getstream.video.android.util.config.types.StreamEnvironment @@ -94,67 +106,105 @@ fun LoginScreen( autoLogIn: Boolean = true, navigateToCallJoin: () -> Unit, ) { - val uiState by loginViewModel.uiState.collectAsState(initial = LoginUiState.Nothing) - val isLoading by remember(uiState) { - mutableStateOf(uiState !is LoginUiState.Nothing && uiState !is LoginUiState.SignInFailure) - } - var isShowingEmailLoginDialog by remember { mutableStateOf(false) } + VideoTheme { + LockScreenOrientation(orientation = Configuration.ORIENTATION_PORTRAIT) + val uiState by loginViewModel.uiState.collectAsState(initial = LoginUiState.Nothing) + val isLoading by remember(uiState) { + mutableStateOf( + uiState !is LoginUiState.Nothing && uiState !is LoginUiState.SignInFailure, + ) + } + val selectedEnv by AppConfig.currentEnvironment.collectAsStateWithLifecycle() + val availableEnvs by remember { mutableStateOf(AppConfig.availableEnvironments) } + val availableLogins = listOf("google", "email", "guest") - HandleLoginUiStates( - autoLogIn = autoLogIn, - loginUiState = uiState, - navigateToCallJoin = navigateToCallJoin, - ) + var isShowingEmailLoginDialog by remember { mutableStateOf(false) } - LoginContent( - autoLogIn = autoLogIn, - isLoading = isLoading, - showEmailLoginDialog = { isShowingEmailLoginDialog = true }, - ) + HandleLoginUiStates( + autoLogIn = autoLogIn, + loginUiState = uiState, + navigateToCallJoin = navigateToCallJoin, + ) - if (isShowingEmailLoginDialog) { - EmailLoginDialog( - onDismissRequest = { isShowingEmailLoginDialog = false }, + LoginContent( + availableEnvs = availableEnvs, + selectedEnv = selectedEnv, + availableLogins = availableLogins, + autoLogIn = autoLogIn, + isLoading = isLoading, + showEmailLoginDialog = { isShowingEmailLoginDialog = true }, + reloadSdk = { + loginViewModel.reloadSdk() + }, + login = { autoLoginBoolean, event -> + autoLoginBoolean?.let { + loginViewModel.autoLogIn = it + } + if (event == null) { + loginViewModel.signInIfValidUserExist() + } else { + loginViewModel.handleUiEvent(event) + } + }, ) + + if (isShowingEmailLoginDialog) { + EmailLoginDialog( + login = { autoLoginBoolean, event -> + autoLoginBoolean?.let { + loginViewModel.autoLogIn = it + } + event?.let { + loginViewModel.handleUiEvent(event) + } + }, + onDismissRequest = { isShowingEmailLoginDialog = false }, + ) + } } } +@OptIn(ExperimentalComposeUiApi::class) @Composable private fun LoginContent( autoLogIn: Boolean, isLoading: Boolean, - showEmailLoginDialog: () -> Unit, - loginViewModel: LoginViewModel = hiltViewModel(), + showEmailLoginDialog: () -> Unit = {}, + reloadSdk: () -> Unit = {}, + login: (Boolean?, LoginEvent?) -> Unit = { _, _ -> }, + availableEnvs: List, + selectedEnv: StreamEnvironment?, + availableLogins: List, ) { - Box(modifier = Modifier.fillMaxSize()) { - val selectedEnv by AppConfig.currentEnvironment.collectAsStateWithLifecycle() - val availableEnvs by AppConfig.availableEnvironments.collectAsStateWithLifecycle() - + Column( + modifier = Modifier + .fillMaxSize() + .background(color = VideoTheme.colors.baseSheetPrimary), + verticalArrangement = Arrangement.SpaceBetween, + ) { selectedEnv?.let { - Box(modifier = Modifier.align(Alignment.TopEnd)) { + Box(modifier = Modifier.align(Alignment.End)) { SelectableDialog( items = availableEnvs, selectedItem = it, onItemSelected = { env -> AppConfig.selectEnv(env) - loginViewModel.reloadSdk() + reloadSdk() }, ) } } Column( modifier = Modifier - .align(Alignment.Center) .wrapContentHeight() .fillMaxWidth() - .background(Colors.background) .semantics { testTagsAsResourceId = true }, verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { Image( - modifier = Modifier.size(102.dp), - painter = painterResource(id = R.drawable.ic_stream_video_meeting_logo), + modifier = Modifier.size(width = 254.dp, height = 179.dp), + painter = painterResource(id = R.drawable.stream_calls_logo), contentDescription = null, ) @@ -163,42 +213,60 @@ private fun LoginContent( Text( modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, - text = stringResource(id = R.string.app_name), + text = buildAnnotatedString { + append("Stream\n") + append( + AnnotatedString( + "[Video Calling]\n", + spanStyle = SpanStyle(VideoTheme.colors.brandGreen), + ), + ) + append(selectedEnv?.displayName ?: "") + }, color = Color.White, - fontSize = 38.sp, + fontSize = 24.sp, ) Spacer(modifier = Modifier.height(30.dp)) + } + Column( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .background( + color = VideoTheme.colors.baseSheetSecondary, + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), + ) + .padding(horizontal = 24.dp, vertical = 32.dp), + ) { if (!isLoading) { - val availableLogins by AppConfig.availableLogins.collectAsStateWithLifecycle() - availableLogins.forEach { when (it) { "google" -> { StreamButton( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .padding(horizontal = 55.dp), + icon = ImageVector.vectorResource(R.drawable.google_button_logo), + modifier = Modifier.fillMaxWidth(), enabled = !isLoading, text = stringResource(id = R.string.sign_in_google), + style = ButtonStyles.primaryButtonStyle() + .copy( + iconStyle = IconStyles.customColorIconStyle( + color = Color.Unspecified, + ), + ), onClick = { - loginViewModel.autoLogIn = false - loginViewModel.handleUiEvent(LoginEvent.GoogleSignIn()) + login(false, LoginEvent.GoogleSignIn()) }, ) } "email" -> { StreamButton( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .padding(horizontal = 55.dp), + modifier = Modifier.fillMaxWidth(), + icon = Icons.Default.Email, enabled = !isLoading, text = stringResource(id = R.string.sign_in_email), + style = ButtonStyles.primaryButtonStyle(), onClick = { - loginViewModel.autoLogIn = true showEmailLoginDialog.invoke() }, ) @@ -206,40 +274,19 @@ private fun LoginContent( "guest" -> { StreamButton( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .padding(horizontal = 55.dp), + modifier = Modifier.fillMaxWidth(), + icon = Icons.Outlined.GroupAdd, enabled = !isLoading, - text = stringResource(R.string.random_user_sign_in), + text = stringResource(id = R.string.random_user_sign_in), + style = ButtonStyles.tetriaryButtonStyle(), onClick = { - loginViewModel.autoLogIn = true - loginViewModel.signInIfValidUserExist() + login(true, null) }, ) } } - Spacer(modifier = Modifier.height(15.dp)) + Spacer(modifier = Modifier.height(VideoTheme.dimens.spacingM)) } - - val context = LocalContext.current - LinkText( - linkTextData = listOf( - LinkTextData(text = stringResource(id = R.string.sign_in_contact)), - LinkTextData( - text = stringResource( - id = R.string.sign_in_contact_us, - ), - tag = "contact us", - annotation = "https://getstream.io/video/docs/", - onClick = { - val intent = Intent(Intent.ACTION_VIEW) - intent.data = Uri.parse(it.item) - startActivity(context, intent, null) - }, - ), - ), - ) } if (BuildConfig.BUILD_TYPE == "benchmark") { @@ -249,10 +296,9 @@ private fun LoginContent( .padding(horizontal = 55.dp) .testTag("authenticate"), text = "Login for Benchmark", + style = ButtonStyles.secondaryButtonStyle(), onClick = { - loginViewModel.handleUiEvent( - LoginEvent.SignInSuccess("benchmark.test@getstream.io"), - ) + login(null, LoginEvent.SignInSuccess("benchmark.test@getstream.io")) }, ) } @@ -260,8 +306,8 @@ private fun LoginContent( if (isLoading) { CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center), - color = VideoTheme.colors.primaryAccent, + modifier = Modifier.align(Alignment.CenterHorizontally), + color = VideoTheme.colors.brandPrimary, ) } } @@ -269,52 +315,31 @@ private fun LoginContent( @Composable private fun EmailLoginDialog( - onDismissRequest: () -> Unit, - loginViewModel: LoginViewModel = hiltViewModel(), + onDismissRequest: () -> Unit = {}, + login: (Boolean?, LoginEvent?) -> Unit = { _, _ -> }, ) { - var email by remember { mutableStateOf("") } + var email by remember { mutableStateOf(TextFieldValue("")) } - Dialog( - onDismissRequest = { onDismissRequest.invoke() }, + StreamDialogPositiveNegative( + style = StreamDialogStyles.defaultDialogStyle(), + onDismiss = { onDismissRequest.invoke() }, + icon = Icons.Default.Email, + title = stringResource(R.string.enter_your_email_address), content = { - Surface( - modifier = Modifier.width(300.dp), - ) { - Column(modifier = Modifier.background(Colors.background)) { - TextField( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - value = email, - onValueChange = { email = it }, - label = { Text(text = stringResource(R.string.enter_your_email_address)) }, - colors = TextFieldDefaults.textFieldColors( - textColor = Colors.description, - focusedLabelColor = VideoTheme.colors.primaryAccent, - unfocusedLabelColor = VideoTheme.colors.textLowEmphasis, - unfocusedIndicatorColor = VideoTheme.colors.primaryAccent, - focusedIndicatorColor = VideoTheme.colors.primaryAccent, - cursorColor = Colors.description, - ), - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Email, - ), - ) - - StreamButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - onClick = { - val userId = UserHelper.getUserIdFromEmail(email) - loginViewModel.handleUiEvent(LoginEvent.SignInSuccess(userId)) - }, - text = "Log in", - ) - - Spacer(modifier = Modifier.height(12.dp)) - } - } + StreamTextField( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + value = email, + onValueChange = { email = it }, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Email, + ), + ) + }, + positiveButton = Triple("Login", ButtonStyles.secondaryButtonStyle()) { + val userId = UserHelper.getUserIdFromEmail(email.text) + login(true, LoginEvent.SignInSuccess(userId)) }, ) } @@ -336,35 +361,45 @@ fun SelectableDialog( color = Color.White, ) if (items.size > 1) { - StreamButton( - text = "Change", - onClick = { showDialog = true }, + StreamIconToggleButton( + toggleState = rememberUpdatedState(newValue = ToggleableState(showDialog)), + onClick = { showDialog = !showDialog }, + onIcon = Icons.Default.Settings, + offIcon = Icons.Default.Settings, + onStyle = ButtonStyles.secondaryIconButtonStyle(), + offStyle = ButtonStyles.primaryIconButtonStyle(), modifier = Modifier.padding(16.dp), ) if (showDialog) { - AlertDialog( - onDismissRequest = { showDialog = false }, - title = { - Text("Available environments") - }, - text = { - FlowRow { - items.forEach { item -> - StreamButton( - text = item.displayName, - onClick = { - onItemSelected(item) - selectedText = item.displayName - showDialog = false - }, - modifier = Modifier.padding(8.dp), - ) - } + Popup( + onDismissRequest = { showDialog = !showDialog }, + alignment = Alignment.TopEnd, + offset = IntOffset( + 0, + (VideoTheme.dimens.componentHeightL + VideoTheme.dimens.spacingL).toPx() + .toInt(), + ), + ) { + Column( + Modifier.background( + color = VideoTheme.colors.baseSheetTertiary, + shape = VideoTheme.shapes.dialog, + ).width(180.dp), + ) { + items.forEach { item -> + StreamButton( + text = item.displayName, + onClick = { + onItemSelected(item) + selectedText = item.displayName + showDialog = !showDialog + }, + style = ButtonStyles.tetriaryButtonStyle(), + modifier = Modifier.padding(horizontal = 8.dp).fillMaxWidth(), + ) } - }, - confirmButton = {}, - dismissButton = {}, - ) + } + } } } } @@ -431,6 +466,37 @@ private fun HandleLoginUiStates( @Composable private fun LoginScreenPreview() { VideoTheme { - LoginScreen {} + val env = StreamEnvironment(env = "demo", displayName = "Demo") + LoginContent( + autoLogIn = false, + isLoading = false, + availableEnvs = listOf(env), + selectedEnv = env, + availableLogins = listOf("google", "email", "guest"), + ) + } +} + +@Preview +@Composable +private fun EmailDialogPreview() { + VideoTheme { + val env = StreamEnvironment(env = "demo", displayName = "Demo") + EmailLoginDialog() + } +} + +@Preview +@Composable +private fun SelectEnvOption() { + VideoTheme { + val env = StreamEnvironment(env = "demo", displayName = "Demo") + val env2 = StreamEnvironment(env = "pronto", displayName = "Pronto") + + SelectableDialog( + items = listOf(env, env2), + selectedItem = env, + onItemSelected = {}, + ) } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt index b40b96f316..bfc8f3da12 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -79,7 +79,7 @@ class LoginViewModel @Inject constructor( public fun reloadSdk() { viewModelScope.launch { - StreamVideoInitHelper.loadSdk(dataStore) + StreamVideoInitHelper.reloadSdk(dataStore) } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt new file mode 100644 index 0000000000..54a20bf6ed --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2014-2024 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.menu + +import android.media.MediaCodecInfo +import android.os.Build +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.MobileScreenShare +import androidx.compose.material.icons.automirrored.filled.ReadMore +import androidx.compose.material.icons.filled.Audiotrack +import androidx.compose.material.icons.filled.AutoGraph +import androidx.compose.material.icons.filled.BluetoothAudio +import androidx.compose.material.icons.filled.BlurOff +import androidx.compose.material.icons.filled.BlurOn +import androidx.compose.material.icons.filled.Feedback +import androidx.compose.material.icons.filled.Headphones +import androidx.compose.material.icons.filled.HeadsetMic +import androidx.compose.material.icons.filled.PortableWifiOff +import androidx.compose.material.icons.filled.RestartAlt +import androidx.compose.material.icons.filled.SettingsVoice +import androidx.compose.material.icons.filled.SpeakerPhone +import androidx.compose.material.icons.filled.SwitchLeft +import androidx.compose.material.icons.filled.VideoFile +import androidx.compose.material.icons.filled.VideoLibrary +import androidx.compose.material.icons.filled.VideoSettings +import io.getstream.video.android.core.audio.StreamAudioDevice +import io.getstream.video.android.ui.menu.base.ActionMenuItem +import io.getstream.video.android.ui.menu.base.DynamicSubMenuItem +import io.getstream.video.android.ui.menu.base.MenuItem +import io.getstream.video.android.ui.menu.base.SubMenuItem + +/** + * Defines the default Stream menu for the demo app. + */ +fun defaultStreamMenu( + showDebugOptions: Boolean = false, + codecList: List, + onCodecSelected: (MediaCodecInfo) -> Unit, + isScreenShareEnabled: Boolean, + isBackgroundBlurEnabled: Boolean, + onToggleScreenShare: () -> Unit = {}, + onShowCallStats: () -> Unit, + onToggleBackgroundBlurClick: () -> Unit, + onToggleAudioFilterClick: () -> Unit, + onRestartSubscriberIceClick: () -> Unit, + onRestartPublisherIceClick: () -> Unit, + onKillSfuWsClick: () -> Unit, + onSwitchSfuClick: () -> Unit, + onShowFeedback: () -> Unit, + onDeviceSelected: (StreamAudioDevice) -> Unit, + availableDevices: List, + loadRecordings: suspend () -> List, +) = buildList { + add( + DynamicSubMenuItem( + title = "Recordings", + icon = Icons.Default.VideoLibrary, + itemsLoader = loadRecordings, + ), + ) + add( + ActionMenuItem( + title = "Call stats", + icon = Icons.Default.AutoGraph, + action = onShowCallStats, + ), + ) + add( + ActionMenuItem( + title = if (isBackgroundBlurEnabled) "Disable background blur" else "Enable background blur", + icon = if (isBackgroundBlurEnabled) Icons.Default.BlurOff else Icons.Default.BlurOn, + action = onToggleBackgroundBlurClick, + ), + ) + add( + SubMenuItem( + title = "Choose audio device", + icon = Icons.Default.SettingsVoice, + items = availableDevices.map { + val icon = when (it) { + is StreamAudioDevice.BluetoothHeadset -> Icons.Default.BluetoothAudio + is StreamAudioDevice.Earpiece -> Icons.Default.Headphones + is StreamAudioDevice.Speakerphone -> Icons.Default.SpeakerPhone + is StreamAudioDevice.WiredHeadset -> Icons.Default.HeadsetMic + } + ActionMenuItem( + title = it.name, + icon = icon, + action = { onDeviceSelected(it) }, + ) + }, + ), + ) + add( + ActionMenuItem( + title = "Feedback", + icon = Icons.Default.Feedback, + action = onShowFeedback, + ), + ) + add( + ActionMenuItem( + title = if (isScreenShareEnabled) "Stop screen-share" else "Start screen-share", + icon = Icons.AutoMirrored.Default.MobileScreenShare, + action = onToggleScreenShare, + ), + ) + if (showDebugOptions) { + add( + SubMenuItem( + title = "Debug options", + icon = Icons.AutoMirrored.Default.ReadMore, + items = debugSubmenu( + codecList, + onCodecSelected, + onToggleAudioFilterClick, + onRestartSubscriberIceClick, + onRestartPublisherIceClick, + onKillSfuWsClick, + onSwitchSfuClick, + ), + ), + ) + } +} + +/** + * Lists the available codecs for this device as list of [MenuItem] + */ +fun codecMenu(codecList: List, onCodecSelected: (MediaCodecInfo) -> Unit) = + codecList.map { + val isHw = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + it.isHardwareAccelerated + } else { + false + } + ActionMenuItem( + title = it.name, + icon = Icons.Default.VideoFile, + highlight = isHw, + action = { onCodecSelected(it) }, + ) + } + +/** + * Optionally defines the debug sub-menu of the demo app. + */ +fun debugSubmenu( + codecList: List, + onCodecSelected: (MediaCodecInfo) -> Unit, + onToggleAudioFilterClick: () -> Unit, + onRestartSubscriberIceClick: () -> Unit, + onRestartPublisherIceClick: () -> Unit, + onKillSfuWsClick: () -> Unit, + onSwitchSfuClick: () -> Unit, +) = listOf( + SubMenuItem( + title = "Available video codecs", + icon = Icons.Default.VideoSettings, + items = codecMenu(codecList, onCodecSelected), + ), + ActionMenuItem( + title = "Toggle audio filter", + icon = Icons.Default.Audiotrack, + action = onToggleAudioFilterClick, + ), + ActionMenuItem( + title = "Restart subscriber Ice", + icon = Icons.Default.RestartAlt, + action = onRestartSubscriberIceClick, + ), + ActionMenuItem( + title = "Restart publisher Ice", + icon = Icons.Default.RestartAlt, + action = onRestartPublisherIceClick, + ), + ActionMenuItem( + title = "Shut down SFU web-socket", + icon = Icons.Default.PortableWifiOff, + action = onKillSfuWsClick, + ), + ActionMenuItem( + title = "Switch SFU", + icon = Icons.Default.SwitchLeft, + action = onSwitchSfuClick, + ), +) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt new file mode 100644 index 0000000000..8d43d2fa49 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2014-2024 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.menu + +import android.Manifest +import android.app.Activity +import android.app.DownloadManager +import android.content.Context +import android.content.Context.DOWNLOAD_SERVICE +import android.graphics.Bitmap +import android.media.MediaCodecList +import android.media.projection.MediaProjectionManager +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.VideoFile +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties +import androidx.core.content.ContextCompat.getSystemService +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionStatus +import com.google.accompanist.permissions.rememberPermissionState +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.core.Call +import io.getstream.video.android.core.call.audio.AudioFilter +import io.getstream.video.android.core.call.video.BitmapVideoFilter +import io.getstream.video.android.core.mapper.ReactionMapper +import io.getstream.video.android.tooling.extensions.toPx +import io.getstream.video.android.ui.call.ReactionsMenu +import io.getstream.video.android.ui.menu.base.ActionMenuItem +import io.getstream.video.android.ui.menu.base.DynamicMenu +import io.getstream.video.android.ui.menu.base.MenuItem +import io.getstream.video.android.util.BlurredBackgroundVideoFilter +import io.getstream.video.android.util.SampleAudioFilter +import kotlinx.coroutines.launch +import java.nio.ByteBuffer + +@OptIn(ExperimentalComposeUiApi::class, ExperimentalPermissionsApi::class) +@Composable +internal fun SettingsMenu( + call: Call, + showDebugOptions: Boolean, + isBackgroundBlurEnabled: Boolean, + onDismissed: () -> Unit, + onShowFeedback: () -> Unit, + onToggleBackgroundBlur: () -> Unit, + onShowCallStats: () -> Unit, +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val availableDevices by call.microphone.devices.collectAsStateWithLifecycle() + + val screenSharePermissionResult = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { + if (it.resultCode == Activity.RESULT_OK && it.data != null) { + call.startScreenSharing(it.data!!) + } + onDismissed.invoke() + }, + ) + + val isScreenSharing by call.screenShare.isEnabled.collectAsStateWithLifecycle() + val onScreenShareClick: () -> Unit = { + if (!isScreenSharing) { + scope.launch { + val mediaProjectionManager = + context.getSystemService(MediaProjectionManager::class.java) + screenSharePermissionResult.launch( + mediaProjectionManager.createScreenCaptureIntent(), + ) + } + } else { + call.stopScreenSharing() + } + } + + val onToggleBackgroundBlurClick: () -> Unit = { + onToggleBackgroundBlur() + + if (call.videoFilter == null) { + call.videoFilter = object : BitmapVideoFilter() { + val filter = BlurredBackgroundVideoFilter() + + override fun filter(bitmap: Bitmap) { + filter.applyFilter(bitmap) + } + } + } else { + call.videoFilter = null + } + } + + val onToggleAudioFilterClick: () -> Unit = { + if (call.audioFilter == null) { + call.audioFilter = object : AudioFilter { + override fun filter( + audioFormat: Int, + channelCount: Int, + sampleRate: Int, + sampleData: ByteBuffer, + ) { + SampleAudioFilter.toRoboticVoice(sampleData, channelCount, 0.8f) + } + } + } else { + call.audioFilter = null + } + onDismissed() + } + + val onRestartSubscriberIceClick: () -> Unit = { + call.debug.restartSubscriberIce() + onDismissed.invoke() + Toast.makeText(context, "Restart Subscriber Ice", Toast.LENGTH_SHORT).show() + } + + val onRestartPublisherIceClick: () -> Unit = { + call.debug.restartPublisherIce() + onDismissed.invoke() + Toast.makeText(context, "Restart Publisher Ice", Toast.LENGTH_SHORT).show() + } + + val onKillSfuWsClick: () -> Unit = { + call.debug.doFullReconnection() + onDismissed.invoke() + Toast.makeText(context, "Killing SFU WS. Should trigger reconnect...", Toast.LENGTH_SHORT) + .show() + } + + val onSwitchSfuClick: () -> Unit = { + call.debug.switchSfu() + onDismissed.invoke() + Toast.makeText(context, "Switch sfu", Toast.LENGTH_SHORT).show() + } + + val codecInfos = remember { + MediaCodecList(MediaCodecList.ALL_CODECS).codecInfos.filter { + it.name.contains("encoder") && it.supportedTypes.firstOrNull { + it.contains("video") + } != null + } + } + + val onLoadRecordings: suspend () -> List = storagePermissionAndroidBellow10 { + when (it) { + is PermissionStatus.Granted -> { + { + call.listRecordings().getOrNull()?.recordings?.map { + ActionMenuItem( + title = it.filename, + icon = Icons.Default.VideoFile, + action = { + context.downloadFile(it.url, it.filename) + onDismissed() + }, + ) + } ?: emptyList() + } + } + is PermissionStatus.Denied -> { + { emptyList() } + } + } + } + + Popup( + offset = IntOffset( + 0, + -(VideoTheme.dimens.componentHeightL + VideoTheme.dimens.spacingS).toPx().toInt(), + ), + alignment = Alignment.BottomStart, + onDismissRequest = { onDismissed.invoke() }, + properties = PopupProperties( + usePlatformDefaultWidth = false, + ), + ) { + DynamicMenu( + header = { + ReactionsMenu( + call = call, + reactionMapper = ReactionMapper.defaultReactionMapper(), + ) { + onDismissed() + } + }, + items = defaultStreamMenu( + showDebugOptions = showDebugOptions, + codecList = codecInfos, + availableDevices = availableDevices, + onDeviceSelected = { + call.microphone.select(it) + onDismissed() + }, + onCodecSelected = { + onDismissed() + }, + onShowFeedback = onShowFeedback, + onToggleScreenShare = onScreenShareClick, + onKillSfuWsClick = onKillSfuWsClick, + onRestartPublisherIceClick = onRestartPublisherIceClick, + onRestartSubscriberIceClick = onRestartSubscriberIceClick, + onToggleAudioFilterClick = onToggleAudioFilterClick, + onToggleBackgroundBlurClick = onToggleBackgroundBlurClick, + onSwitchSfuClick = onSwitchSfuClick, + onShowCallStats = onShowCallStats, + isBackgroundBlurEnabled = isBackgroundBlurEnabled, + isScreenShareEnabled = isScreenSharing, + loadRecordings = onLoadRecordings, + ), + ) + } +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +private fun storagePermissionAndroidBellow10( + permission: (PermissionStatus) -> suspend () -> List, +): suspend () -> List { + // Check if the device's API level is below Android 10 (API level 29) + return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + val writeStoragePermissionState = + rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) + LaunchedEffect(key1 = true) { + // Request permission + writeStoragePermissionState.launchPermissionRequest() + } + permission(writeStoragePermissionState.status) + } else { + permission(PermissionStatus.Granted) + } +} + +private fun Context.downloadFile(url: String, title: String) { + val request = DownloadManager.Request(Uri.parse(url)) + .setTitle(title) // Title of the Download Notification + .setDescription("Downloading") // Description of the Download Notification + .setNotificationVisibility( + DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED, + ) // Visibility of the download Notification + .setAllowedOverMetered(true) // Set if download is allowed on Mobile network + .setAllowedOverRoaming(true) // Set if download is allowed on Roaming network + .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, title) + + val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager + downloadManager.enqueue(request) // enqueue puts the download request in the queue. +} + +@Preview +@Composable +private fun SettingsMenuPreview() { + VideoTheme { + DynamicMenu( + items = defaultStreamMenu( + codecList = emptyList(), + onCodecSelected = { + }, + isScreenShareEnabled = false, + isBackgroundBlurEnabled = true, + onToggleScreenShare = { }, + onShowCallStats = { }, + onToggleBackgroundBlurClick = { }, + onToggleAudioFilterClick = { }, + onRestartSubscriberIceClick = { }, + onRestartPublisherIceClick = { }, + onKillSfuWsClick = { }, + onSwitchSfuClick = { }, + availableDevices = emptyList(), + onDeviceSelected = { + }, + onShowFeedback = {}, + loadRecordings = { emptyList() }, + ), + ) + } +} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt new file mode 100644 index 0000000000..4858917964 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2014-2024 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.menu.base + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +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.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamToggleButton +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize +import io.getstream.video.android.ui.menu.debugSubmenu +import io.getstream.video.android.ui.menu.defaultStreamMenu + +/** + * A composable capable of loading a menu based on a list structure of menu items and sub menus. + * There are three types of items: + * - [ActionMenuItem] - shown normally as an item that can be clicked. + * - [SubMenuItem] - that contains another list of [ActionMenuItem] o [SubMenuItem] which will be shown when clicked. + * - [DynamicSubMenuItem] - that shows a spinner and calls a loading function before behaving as [SubMenuItem] + * + * The transition and history between the items is automatic. + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun DynamicMenu(header: (@Composable LazyItemScope.() -> Unit)? = null, items: List) { + val history = remember { mutableStateListOf>() } + val dynamicItems = remember { mutableStateListOf() } + var loadedItems by remember { mutableStateOf(false) } + Box( + modifier = Modifier + .fillMaxWidth() + .background( + color = VideoTheme.colors.baseSheetPrimary, + shape = VideoTheme.shapes.dialog, + ), + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .background( + shape = VideoTheme.shapes.sheet, + color = VideoTheme.colors.baseSheetPrimary, + ) + .padding(12.dp), + ) { + if (history.isEmpty()) { + header?.let { + item(content = header) + } + menuItems(items) { + history.add(Pair(it.title, it)) + } + } else { + val lastContent = history.last() + stickyHeader { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .background(VideoTheme.colors.baseSheetPrimary) + .fillMaxWidth(), + ) { + IconButton(onClick = { history.removeLastOrNull() }) { + Icon( + tint = VideoTheme.colors.basePrimary, + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = "Back", + ) + } + Text( + text = lastContent.first, + style = VideoTheme.typography.subtitleS, + color = VideoTheme.colors.basePrimary, + ) + } + } + + val subMenu = lastContent.second + val dynamicMenu = subMenu as? DynamicSubMenuItem + + if (dynamicMenu != null) { + if (!loadedItems) { + dynamicItems.clear() + loadingItems(dynamicMenu) { + loadedItems = true + dynamicItems.addAll(it) + } + } + if (dynamicItems.isNotEmpty()) { + menuItems(dynamicItems) { + history.add(Pair(it.title, it)) + } + } else if (loadedItems) { + noItems() + } + } else { + if (subMenu.items.isEmpty()) { + noItems() + } else { + menuItems(subMenu.items) { + history.add(Pair(it.title, it)) + } + } + } + } + } + } +} + +private fun LazyListScope.loadingItems( + dynamicMenu: DynamicSubMenuItem, + onLoaded: (List) -> Unit, +) { + item { + LaunchedEffect(key1 = dynamicMenu) { + onLoaded(dynamicMenu.itemsLoader.invoke()) + } + LinearProgressIndicator( + modifier = Modifier + .padding(33.dp) + .fillMaxWidth(), + color = VideoTheme.colors.basePrimary, + ) + } +} + +private fun LazyListScope.noItems() { + item { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + textAlign = TextAlign.Center, + text = "No items", + style = VideoTheme.typography.subtitleS, + color = VideoTheme.colors.basePrimary, + ) + } +} + +private fun LazyListScope.menuItems( + items: List, + onNewSubmenu: (SubMenuItem) -> Unit, +) { + items(items.size) { index -> + val item = items[index] + val highlight = item.highlight + StreamToggleButton( + onText = item.title, + offText = item.title, + onIcon = item.icon, + onStyle = VideoTheme.styles.buttonStyles.toggleButtonStyleOn(StyleSize.XS).copy( + iconStyle = VideoTheme.styles.iconStyles.customColorIconStyle( + color = if (highlight) VideoTheme.colors.brandPrimary else VideoTheme.colors.basePrimary, + ), + ), + onClick = { + val actionItem = item as? ActionMenuItem + actionItem?.action?.invoke() + val menuItem = item as? SubMenuItem + menuItem?.let { + onNewSubmenu(it) + } + }, + ) + } +} + +@Preview +@Composable +private fun DynamicMenuPreview() { + VideoTheme { + DynamicMenu( + items = defaultStreamMenu( + codecList = emptyList(), + onCodecSelected = {}, + isScreenShareEnabled = false, + isBackgroundBlurEnabled = true, + onToggleScreenShare = { }, + onShowCallStats = { }, + onToggleBackgroundBlurClick = { }, + onToggleAudioFilterClick = { }, + onRestartSubscriberIceClick = { }, + onRestartPublisherIceClick = { }, + onKillSfuWsClick = { }, + onSwitchSfuClick = { }, + availableDevices = emptyList(), + onDeviceSelected = {}, + onShowFeedback = {}, + loadRecordings = { emptyList() }, + ), + ) + } +} + +@Preview +@Composable +private fun DynamicMenuDebugOptionPreview() { + VideoTheme { + DynamicMenu( + items = defaultStreamMenu( + showDebugOptions = true, + codecList = emptyList(), + onCodecSelected = {}, + isScreenShareEnabled = true, + isBackgroundBlurEnabled = true, + onToggleScreenShare = { }, + onShowCallStats = { }, + onToggleBackgroundBlurClick = { }, + onToggleAudioFilterClick = { }, + onRestartSubscriberIceClick = { }, + onRestartPublisherIceClick = { }, + onKillSfuWsClick = { }, + onSwitchSfuClick = { }, + availableDevices = emptyList(), + onDeviceSelected = {}, + onShowFeedback = {}, + loadRecordings = { emptyList() }, + ), + ) + } +} + +@Preview +@Composable +private fun DynamicMenuDebugPreview() { + VideoTheme { + DynamicMenu( + items = debugSubmenu( + codecList = emptyList(), + onCodecSelected = {}, + onKillSfuWsClick = { }, + onRestartPublisherIceClick = { }, + onRestartSubscriberIceClick = { }, + onToggleAudioFilterClick = { }, + onSwitchSfuClick = { }, + ), + ) + } +} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/MenuTypes.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/MenuTypes.kt new file mode 100644 index 0000000000..21e72d695f --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/MenuTypes.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2014-2024 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.menu.base + +import androidx.compose.ui.graphics.vector.ImageVector + +/** + * Parent class on all menu items. + * + * @param title - title of the item, used to display in the menu, or a subtitle to the sub menu. + * @param icon - the icon to be shown with the item. + * @param highlight - if the icon should be highlighted or not (usually tinted with primary color) + */ +abstract class MenuItem( + val title: String, + val icon: ImageVector, + val highlight: Boolean = false, +) + +/** + * Same as [MenuItem] but additionally has an action associated with it. + * + * @param action - the action that will execute when the item is clicked. + */ +class ActionMenuItem( + title: String, + icon: ImageVector, + highlight: Boolean = false, + val action: () -> Unit, +) : MenuItem(title, icon, highlight) + +/** + * Unlike the [ActionMenuItem] the [SubMenuItem] contains a list of [MenuItem] that create a new submenu. + * Clicking a [SubMenuItem] will show the [items]. + * + * @param items - the items will be shown in the menu. + */ +open class SubMenuItem(title: String, icon: ImageVector, val items: List) : + MenuItem(title, icon) + +/** + * Similar to the [SubMenuItem] the [DynamicSubMenuItem] contains an [itemsLoader] function to load the items. + * The [DynamicMenu] knows how to invoke this function to dynamically load the items while showing a progress indicator. + * + * @param itemsLoader the items provider function. + */ +class DynamicSubMenuItem( + title: String, + icon: ImageVector, + val itemsLoader: suspend () -> List, +) : SubMenuItem(title, icon, emptyList()) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt index 696f3c3efa..dfdce7207f 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -34,6 +34,8 @@ import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.RadioButton import androidx.compose.material.RadioButtonDefaults import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Call import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -47,12 +49,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.avatar.UserAvatar +import io.getstream.video.android.compose.ui.components.base.StreamButton +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize import io.getstream.video.android.core.R +import io.getstream.video.android.mock.previewUsers import io.getstream.video.android.model.User -import io.getstream.video.android.ui.theme.Colors -import io.getstream.video.android.ui.theme.StreamImageButton +import io.getstream.video.android.models.GoogleAccount @Composable fun DirectCallJoinScreen( @@ -67,7 +71,7 @@ fun DirectCallJoinScreen( Column( modifier = Modifier .fillMaxSize() - .background(Colors.background), + .background(VideoTheme.colors.baseSheetPrimary), horizontalAlignment = Alignment.CenterHorizontally, ) { Header(user = uiState.currentUser) @@ -93,6 +97,7 @@ private fun Header(user: User?) { Row { user?.let { UserAvatar( + textSize = StyleSize.XS, modifier = Modifier.size(24.dp), userName = it.userNameOrId, userImage = it.image, @@ -135,7 +140,7 @@ private fun Body( modifier = Modifier .size(50.dp) .align(Alignment.Center), - color = VideoTheme.colors.primaryAccent, + color = VideoTheme.colors.brandPrimary, ) } else { uiState.googleAccounts?.let { users -> @@ -143,12 +148,15 @@ private fun Body( entries = users, onUserClick = { clickedIndex -> toggleUserSelection(clickedIndex) }, ) - StreamImageButton( // Floating button + StreamButton( + // Floating button modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = 10.dp), enabled = users.any { it.isSelected }, - imageRes = R.drawable.stream_video_ic_call, + icon = Icons.Default.Call, + text = "Start call", + style = VideoTheme.styles.buttonStyles.secondaryButtonStyle(), onClick = { onStartCallClick( users @@ -206,6 +214,7 @@ private fun UserRow( ) { Row(verticalAlignment = Alignment.CenterVertically) { UserAvatar( + textSize = StyleSize.M, modifier = Modifier.size(50.dp), userName = name, userImage = avatarUrl, @@ -222,7 +231,7 @@ private fun UserRow( modifier = Modifier.size(20.dp), onClick = null, colors = RadioButtonDefaults.colors( - selectedColor = VideoTheme.colors.primaryAccent, + selectedColor = VideoTheme.colors.basePrimary, unselectedColor = Color.LightGray, ), ) @@ -234,5 +243,24 @@ private fun UserRow( private fun HeaderPreview() { VideoTheme { Header(user = User(name = "Very very very long user name here")) + Body( + uiState = DirectCallUiState( + googleAccounts = + previewUsers.map { + GoogleAccountUiState( + isSelected = false, + account = GoogleAccount( + it.id, + it.id, + it.name, + null, + false, + ), + ) + }, + ), + toggleUserSelection = {}, + ) { + } } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinViewModel.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinViewModel.kt index 9de12091bd..a964e3be05 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinViewModel.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/Colors.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/Colors.kt deleted file mode 100644 index 0e470fdf6d..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/Colors.kt +++ /dev/null @@ -1,25 +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.theme - -import androidx.compose.ui.graphics.Color - -object Colors { - val background: Color = Color(0xFF2C2C2E) - val secondBackground: Color = Color(0xFF1C1E22) - val description: Color = Color(0xFF979797) -} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/LinkText.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/LinkText.kt deleted file mode 100644 index 042e2cefe2..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/LinkText.kt +++ /dev/null @@ -1,93 +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.theme - -import androidx.compose.foundation.text.ClickableText -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.sp - -data class LinkTextData( - val text: String, - val tag: String? = null, - val annotation: String? = null, - val onClick: ((str: AnnotatedString.Range) -> Unit)? = null, -) - -@Composable -fun LinkText( - linkTextData: List, - modifier: Modifier = Modifier, -) { - val annotatedString = createAnnotatedString(linkTextData) - - ClickableText( - text = annotatedString, - style = TextStyle( - color = Colors.description, - textAlign = TextAlign.Center, - fontSize = 18.sp, - ), - onClick = { offset -> - linkTextData.forEach { annotatedStringData -> - if (annotatedStringData.tag != null && annotatedStringData.annotation != null) { - annotatedString.getStringAnnotations( - tag = annotatedStringData.tag, - start = offset, - end = offset, - ).firstOrNull()?.let { - annotatedStringData.onClick?.invoke(it) - } - } - } - }, - modifier = modifier, - ) -} - -@Composable -private fun createAnnotatedString(data: List): AnnotatedString { - return buildAnnotatedString { - data.forEach { linkTextData -> - if (linkTextData.tag != null && linkTextData.annotation != null) { - pushStringAnnotation( - tag = linkTextData.tag, - annotation = linkTextData.annotation, - ) - withStyle( - style = SpanStyle( - color = Colors.description, - fontSize = 18.sp, - textDecoration = TextDecoration.Underline, - ), - ) { - append(linkTextData.text) - } - pop() - } else { - append(linkTextData.text) - } - } - } -} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/StreamButton.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/StreamButton.kt deleted file mode 100644 index 97afaa3d45..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/StreamButton.kt +++ /dev/null @@ -1,63 +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.theme - -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -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.ui.theme.Colors.description - -@Composable -fun StreamButton( - modifier: Modifier = Modifier, - text: String, - enabled: Boolean = true, - onClick: () -> Unit, -) { - Button( - modifier = modifier.clip(RoundedCornerShape(8.dp)), - enabled = enabled, - onClick = onClick, - colors = ButtonDefaults.buttonColors( - backgroundColor = VideoTheme.colors.primaryAccent, - contentColor = VideoTheme.colors.primaryAccent, - disabledBackgroundColor = description, - disabledContentColor = description, - ), - ) { - Text( - text = text, - color = Color.White, - ) - } -} - -@Preview -@Composable -private fun StreamButtonPreview() { - VideoTheme { - StreamButton(text = "Sign In with Email", onClick = {}) - } -} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/StreamImageButton.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/StreamImageButton.kt deleted file mode 100644 index 929ffad991..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/StreamImageButton.kt +++ /dev/null @@ -1,53 +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.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/demo-app/src/main/kotlin/io/getstream/video/android/util/BlurredBackgroundVideoFilter.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/BlurredBackgroundVideoFilter.kt index 71a18a5a0b..1ab0a453ff 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/BlurredBackgroundVideoFilter.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/BlurredBackgroundVideoFilter.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/FeedbackSender.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/FeedbackSender.kt new file mode 100644 index 0000000000..a4b6383d72 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/FeedbackSender.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2014-2024 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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import java.net.URL + +/** + * A simple http post sender for the feedback request. + */ +class FeedbackSender { + private val client = OkHttpClient() + + fun isValidEmail(email: String): Boolean { + val emailRegex = "^[A-Za-z](.*)([@]{1})(.{1,})(\\.)(.{1,})".toRegex() + return email.matches(emailRegex) + } + + fun sendFeedback(email: String, message: String, callId: String, coroutineScope: CoroutineScope = MainScope(), onFinished: (isError: Boolean) -> Unit) { + coroutineScope.launch(Dispatchers.IO) { + val error = try { + val response = sendFeedbackInternal(email, message, callId) + when (response.code) { + 204 -> false + else -> true + } + } catch (e: Exception) { + true + } + onFinished(error) + } + } + + private fun sendFeedbackInternal(email: String, message: String, callId: String): Response { + val url = URL("https://getstream.io/api/crm/video_feedback/") + val formData = MultipartBody.Builder().apply { + addFormDataPart("email", email) + addFormDataPart("message", message) + addFormDataPart("page_url", "https://www.getstream.io?meeting=true&id=$callId") + setType(MultipartBody.FORM) + } + val request = Request.Builder() + .url(url) + .post(formData.build()) + .header("User-Agent", "StreamDemoApp-Android/1.0.0") + .header("Connection", "keep-alive") + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .build() + + return client.newCall(request).execute() + } +} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/InAppUpdateHelper.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/InAppUpdateHelper.kt index 4ed69e7c23..c8e355ca1c 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/InAppUpdateHelper.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/InAppUpdateHelper.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/InstallReferrer.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/InstallReferrer.kt index d0d878cd43..14b069155b 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/InstallReferrer.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/InstallReferrer.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/LockOrientation.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/LockOrientation.kt new file mode 100644 index 0000000000..b71e9cba4b --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/LockOrientation.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2014-2024 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.app.Activity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.platform.LocalContext + +@Composable +fun LockScreenOrientation(orientation: Int) { + val context = LocalContext.current + DisposableEffect(Unit) { + val activity = context as? Activity ?: return@DisposableEffect onDispose {} + val originalOrientation = activity.requestedOrientation + activity.requestedOrientation = orientation + onDispose { + // restore original orientation when view disappears + activity.requestedOrientation = originalOrientation + } + } +} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/NetworkMonitor.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/NetworkMonitor.kt index 56a920f4d1..55cf16a7d9 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/NetworkMonitor.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/NetworkMonitor.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/SampleAudioFilter.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/SampleAudioFilter.kt index 4bafe1eeb8..9ab322e486 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/SampleAudioFilter.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/SampleAudioFilter.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/SampleVideoFilter.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/SampleVideoFilter.kt index e7979003cc..c91810320c 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/SampleVideoFilter.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/SampleVideoFilter.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt index d76fb9b088..a8b359046d 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -57,6 +57,14 @@ object StreamVideoInitHelper { context = appContext.applicationContext } + suspend fun reloadSdk( + dataStore: StreamUserDataStore, + useRandomUserAsFallback: Boolean = true, + ) { + StreamVideo.removeClient() + loadSdk(dataStore, useRandomUserAsFallback) + } + /** * A helper function that will initialise the [StreamVideo] SDK and also the [ChatClient]. * Set [useRandomUserAsFallback] to true if you want to use a guest fallback if the user is not diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/UserHelper.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/UserHelper.kt index be076f82af..bad1c22536 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/UserHelper.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/UserHelper.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/AppConfig.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/AppConfig.kt index c9ec12477b..274c23db1a 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/AppConfig.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/AppConfig.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -19,27 +19,19 @@ package io.getstream.video.android.util.config import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.SharedPreferences +import android.net.Uri import androidx.core.content.edit -import com.google.firebase.ktx.Firebase -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import com.google.firebase.remoteconfig.ktx.remoteConfig -import com.google.firebase.remoteconfig.ktx.remoteConfigSettings import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import io.getstream.log.taggedLogger -import io.getstream.video.android.BuildConfig -import io.getstream.video.android.R -import io.getstream.video.android.util.config.types.Flavor import io.getstream.video.android.util.config.types.StreamEnvironment -import io.getstream.video.android.util.config.types.StreamRemoteConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -import java.util.concurrent.Executors /** * Main entry point for remote / local configuration @@ -48,25 +40,40 @@ import java.util.concurrent.Executors object AppConfig { // Constants private val logger by taggedLogger("RemoteConfig") - private const val APP_CONFIG_KEY = "appconfig" private const val SHARED_PREF_NAME = "stream_demo_app" - private const val SELECTED_ENV = "selected_env" + private const val SELECTED_ENV = "selected_env_v2" // Data - private lateinit var config: StreamRemoteConfig private lateinit var environment: StreamEnvironment private lateinit var prefs: SharedPreferences // State of config values - public val currentEnvironment = MutableStateFlow(null) - public val availableEnvironments = MutableStateFlow>(arrayListOf()) - public val availableLogins = MutableStateFlow>(arrayListOf()) - - // Utils + val currentEnvironment = MutableStateFlow(null) + val availableEnvironments = listOf( + StreamEnvironment( + env = "pronto", + aliases = listOf("stream-calls-dogfood"), + displayName = "Pronto", + sharelink = "https://pronto.getstream.io/join/", + ), + StreamEnvironment( + env = "demo", + aliases = listOf(""), + displayName = "Demo", + sharelink = "https://getstream.io/video/demos/join/", + ), + StreamEnvironment( + env = "staging", + aliases = emptyList(), + displayName = "Staging", + sharelink = "https://staging.getstream.io/join/", + ), + ) + + // Utilities private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() // API - /** * Setup the remote configuration. * Will automatically put config into [AppConfig.config] @@ -82,51 +89,21 @@ object AppConfig { ) { // Load prefs prefs = context.getSharedPreferences(SHARED_PREF_NAME, MODE_PRIVATE) - - // Initialize local and default values - val remoteConfig = initializeRemoteConfig() - - // Fetch remote - remoteConfig.fetchAndActivate() - .addOnCompleteListener(Executors.newSingleThreadExecutor()) { task -> - if (task.isSuccessful) { - logger.v { "Updated remote config values" } - } else { - logger.e { "Update of remote config failed." } - } - try { - // Parse config - val parsed = parseConfig(remoteConfig) - config = parsed!! - - // Update available logins - availableLogins.value = config.supportedLogins.firstOrNull { - it.flavor.contains(BuildConfig.FLAVOR) - }?.logins ?: arrayListOf("email") - - // Select environment - val jsonAdapter: JsonAdapter = moshi.adapter() - val selectedEnvData = prefs.getString(SELECTED_ENV, null) - var selectedEnvironment = selectedEnvData?.let { - jsonAdapter.fromJson(it) - } - if (selectedEnvironment?.isForFlavor(BuildConfig.FLAVOR) != true) { - // We may have selected environment previously which is no longer available - selectedEnvironment = null - } - val which = selectedEnvironment ?: config.environments.default(BuildConfig.FLAVOR) - selectEnv(which) - availableEnvironments.value = config.environments.filter { - it.isForFlavor(BuildConfig.FLAVOR) - } - currentEnvironment.value = which - coroutineScope.launch { - onLoaded() - } - } catch (e: Exception) { - logger.e(e) { "Failed to parse remote config. Deeplinks not working!" } - } + try { + val jsonAdapter: JsonAdapter = moshi.adapter() + val selectedEnvData = prefs.getString(SELECTED_ENV, null) + val selectedEnvironment = selectedEnvData?.let { + jsonAdapter.fromJson(it) } + val which = selectedEnvironment ?: availableEnvironments[0] + selectEnv(which) + currentEnvironment.value = which + coroutineScope.launch { + onLoaded() + } + } catch (e: Exception) { + logger.e(e) { "Failed to parse remote config. Deeplinks not working!" } + } } /** @@ -135,15 +112,9 @@ object AppConfig { * @param which environment to select */ fun selectEnv(which: StreamEnvironment) { - val currentFlavor = BuildConfig.FLAVOR val jsonAdapter: JsonAdapter = moshi.adapter() - - val selectedEnvironment = which.takeIf { - config.environments.containsForFlavor(it.env!!, currentFlavor) - } - // Select default environment from config if none is in prefs - environment = selectedEnvironment ?: config.environments.default(currentFlavor) + environment = which // Update selected env prefs.edit(commit = true) { putString(SELECTED_ENV, jsonAdapter.toJson(environment)) @@ -151,51 +122,24 @@ object AppConfig { currentEnvironment.value = environment } - // Internal logic - private fun initializeRemoteConfig(): FirebaseRemoteConfig { - val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig - val configSettings = remoteConfigSettings { - minimumFetchIntervalInSeconds = 3600 - } - remoteConfig.setConfigSettingsAsync(configSettings) - remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults) - return remoteConfig - } - - @OptIn(ExperimentalStdlibApi::class) - private fun parseConfig(remoteConfig: FirebaseRemoteConfig): StreamRemoteConfig? { - val value = remoteConfig.getString(APP_CONFIG_KEY) - val jsonAdapter: JsonAdapter = moshi.adapter() - return jsonAdapter.fromJson(value) - } - - private fun List.containsForFlavor(name: String, flavor: String): Boolean { - val found = this.find { - it.env == name && it.flavors.containsFlavorName(flavor) - } - return found != null - } - - private fun List.containsFlavorName(name: String): Boolean { - val found = this.find { - it.flavor!! == name + fun List.fromUri(env: Uri): StreamEnvironment? { + val environmentName = env.extractEnvironment() + return environmentName?.let { name -> + firstOrNull { streamEnv -> + streamEnv.env == name || streamEnv.aliases.contains(name) + } } - return found != null - } - - private fun StreamEnvironment.isForFlavor(flavor: String): Boolean { - return flavors.find { it.flavor == flavor } != null - } - - private fun StreamEnvironment.isDefaultForFlavor(flavor: String): Boolean { - return flavors.find { it.flavor == flavor }?.default == true } - private fun List.default(currentFlavor: String): StreamEnvironment { - return findLast { env -> - env.isDefaultForFlavor(currentFlavor) - } ?: config.environments.find { - it.isForFlavor(currentFlavor) - } ?: config.environments.first() + private fun Uri?.extractEnvironment(): String? { + // Extract the host from the Uri + val host = this?.host ?: return null + // Split the host by "." and return the first part + val parts = host.split(".") + // 0 | 1 | 2 + // | getstream | io + // pronto | getstream | io + // stream-call-dogfood | vercel | app + return if (parts.size > 2) parts[0] else "" } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/Flavor.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/Flavor.kt deleted file mode 100644 index 84c6715a82..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/Flavor.kt +++ /dev/null @@ -1,26 +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.util.config.types - -import androidx.annotation.Keep -import com.squareup.moshi.Json - -@Keep -data class Flavor( - @Json(name = "flavor") var flavor: String? = null, - @Json(name = "default") var default: Boolean? = null, -) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamEnvironment.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamEnvironment.kt index 7e6ee2ecf5..1b4a3e540d 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamEnvironment.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamEnvironment.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. * * Licensed under the Stream License; * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import com.squareup.moshi.Json @Keep data class StreamEnvironment( @Json(name = "env") var env: String, + @Json(name = "aliases") var aliases: List = emptyList(), @Json(name = "displayName") var displayName: String, - @Json(name = "flavors") var flavors: List = arrayListOf(), + @Json(name = "sharelink") var sharelink: String? = null, ) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamRemoteConfig.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamRemoteConfig.kt deleted file mode 100644 index 61061d95f4..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamRemoteConfig.kt +++ /dev/null @@ -1,26 +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.util.config.types - -import androidx.annotation.Keep -import com.squareup.moshi.Json - -@Keep -data class StreamRemoteConfig( - @Json(name = "supportedLogins") var supportedLogins: List = arrayListOf(), - @Json(name = "environments") var environments: List = arrayListOf(), -) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/SupportedLogins.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/SupportedLogins.kt deleted file mode 100644 index 1350e2aab1..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/SupportedLogins.kt +++ /dev/null @@ -1,26 +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.util.config.types - -import androidx.annotation.Keep -import com.squareup.moshi.Json - -@Keep -data class SupportedLogins( - @Json(name = "flavor") var flavor: List = arrayListOf(), - @Json(name = "logins") var logins: List = arrayListOf(), -) diff --git a/demo-app/src/main/res/drawable/feedback_artwork.png b/demo-app/src/main/res/drawable/feedback_artwork.png new file mode 100644 index 0000000000..995dedb26d Binary files /dev/null and b/demo-app/src/main/res/drawable/feedback_artwork.png differ diff --git a/demo-app/src/main/res/drawable/google_button_logo.xml b/demo-app/src/main/res/drawable/google_button_logo.xml new file mode 100644 index 0000000000..cb435e1a46 --- /dev/null +++ b/demo-app/src/main/res/drawable/google_button_logo.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/demo-app/src/main/res/drawable/ic_blur_off.xml b/demo-app/src/main/res/drawable/ic_blur_off.xml index 434201fd57..ab47fb8354 100644 --- a/demo-app/src/main/res/drawable/ic_blur_off.xml +++ b/demo-app/src/main/res/drawable/ic_blur_off.xml @@ -1,6 +1,6 @@ + + + + + + + + diff --git a/demo-app/src/main/res/drawable/ic_layout_spotlight.xml b/demo-app/src/main/res/drawable/ic_layout_spotlight.xml new file mode 100644 index 0000000000..9709c02dbe --- /dev/null +++ b/demo-app/src/main/res/drawable/ic_layout_spotlight.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/demo-app/src/main/res/drawable/ic_mic.xml b/demo-app/src/main/res/drawable/ic_mic.xml index 004fc14cf3..dc96becfc2 100644 --- a/demo-app/src/main/res/drawable/ic_mic.xml +++ b/demo-app/src/main/res/drawable/ic_mic.xml @@ -1,6 +1,6 @@ - - - appconfig - {"supportedLogins":[{"flavor":["development","production"],"logins":["google","email","guest"]}],"environments":[{"env":"demo","displayName":"Demo","flavors":[{"flavor":"development","default":false},{"flavor":"production","default":true}]},{"env":"pronto","displayName":"Pronto","flavors":[{"flavor":"development","default":true}]}]} - - \ No newline at end of file diff --git a/demo-app/src/production/res/values/strings.xml b/demo-app/src/production/res/values/strings.xml index 3cbf296626..3f96204c07 100644 --- a/demo-app/src/production/res/values/strings.xml +++ b/demo-app/src/production/res/values/strings.xml @@ -1,6 +1,6 @@ + + + diff --git a/stream-video-android-ui-core/src/main/res/drawable/stream_video_ic_landscape_mode.xml b/stream-video-android-ui-core/src/main/res/drawable/stream_video_ic_landscape_mode.xml index a6acefbe70..978a643409 100644 --- a/stream-video-android-ui-core/src/main/res/drawable/stream_video_ic_landscape_mode.xml +++ b/stream-video-android-ui-core/src/main/res/drawable/stream_video_ic_landscape_mode.xml @@ -1,6 +1,6 @@ + + + #000000 + #72767E + #B4B7BB + #DBDDE1 + #E9EAED + #FFFFFF + #FFFFFF + #E9F1FF + #80000000 + #99000000 + #005FFF + #A60C0D0E + #A60C0D0E + #FFFFFF + #D9000000 + #FCFCFC + #FF3742 + #20E070 + #005FFF + #123D82 + #1E262E + #FBF4DD + #FFFFFF + #545A64 + #272A30 + #FFFFFF + #000000 + #FFFFFF + #FFFFFF + #000000 + #4C525C + #FFFFFF + #1E262E + #005FFF + #A60C0D0E + #00E2A1 + #FFFFFF + #DC433B + #FF3742 + + + #FFFFFF + #72767E + #4C525C + #272A30 + #1C1E22 + #000000 + #121416 + #00193D + #33000000 + #99FFFFFF + #337EFF + #FF3742 + #20E070 + #302D22 + #FFFFFF + #FFFFFF + #000000 + #000000 + #FFFFFF + #4C525C + #FFFFFF + #FFFFFF + #005FFF + #A60C0D0E + #FF3742 + #A60C0D0E + #A60C0D0E + + + #FF8A65 + #81C784 + #4FC3F7 + #A1887F + #FFF176 + #E57373 + + @color/stream_video_brand_cyan + @color/stream_video_brand_green + @color/stream_video_brand_primary + @color/stream_video_brand_primary_lt + @color/stream_video_brand_red + @color/stream_video_brand_red_lt + @color/stream_video_brand_violet + @color/stream_video_brand_yellow + + diff --git a/stream-video-android-ui-core/src/main/res/values/colors.xml b/stream-video-android-ui-core/src/main/res/values/colors.xml index ea2da16896..69b7a1360c 100644 --- a/stream-video-android-ui-core/src/main/res/values/colors.xml +++ b/stream-video-android-ui-core/src/main/res/values/colors.xml @@ -1,6 +1,6 @@ - - #000000 - #72767E - #B4B7BB - #DBDDE1 - #E9EAED - #FFFFFF - #FFFFFF - #E9F1FF - #80000000 - #99000000 - #005FFF - #A60C0D0E - #A60C0D0E - #FFFFFF - #D9000000 - #FCFCFC - #FF3742 - #20E070 - #005FFF - #123D82 - #1E262E - #FBF4DD - #FFFFFF - #545A64 - #272A30 - #FFFFFF - #000000 - #FFFFFF - #FFFFFF - #000000 - #4C525C - #FFFFFF - #1E262E - #005FFF - #A60C0D0E - #00E2A1 - #FFFFFF - #DC433B - #FF3742 + + #005FFF + #4C8FFF + #123D82 + #1B2C43 + #1E262E29 + #69E5F6 + #00E2A1 + #FFD646 + #DC433B + #E36962 + #6A3233 + #31292F + #B38AF8 - - #FFFFFF - #72767E - #4C525C - #272A30 - #1C1E22 - #000000 - #121416 - #00193D - #33000000 - #99FFFFFF - #337EFF - #FF3742 - #20E070 - #302D22 - #FFFFFF - #FFFFFF - #000000 - #000000 - #FFFFFF - #4C525C - #FFFFFF - #FFFFFF - #005FFF - #A60C0D0E - #FF3742 - #A60C0D0E - #A60C0D0E + + #E3E4E5 + #979CA0 + #4C535B + #656B72 + #7E8389 + #323B44 + #101213 + #0C0D0E + #19232D + #A60C0D0E + #0C0D0EA6 - - #FF8A65 - #81C784 - #4FC3F7 - #A1887F - #FFF176 - #E57373 - - @color/stream_video_avatar_gradient_orange - @color/stream_video_avatar_gradient_green - @color/stream_video_avatar_gradient_blue - @color/stream_video_avatar_gradient_brown - @color/stream_video_avatar_gradient_yellow - @color/stream_video_avatar_gradient_red - - + + @color/stream_video_base_sheet_tetriary + @color/stream_video_base_sheet_secondary + @color/stream_video_brand_secondary_transparent + @color/stream_video_brand_primary + @color/stream_video_brand_primary_dk + @color/stream_video_brand_secondary + @color/stream_video_brand_red + @color/stream_video_brand_red_dk + @color/stream_video_brand_maroon + + + @color/stream_video_base_primary + @color/stream_video_base_quaternary + @color/stream_video_brand_primary + @color/stream_video_brand_red + @color/stream_video_base_quinary + + + @color/stream_video_brand_green + @color/stream_video_brand_yellow + @color/stream_video_brand_red + + \ No newline at end of file diff --git a/stream-video-android-ui-core/src/main/res/values/dimens-legacy.xml b/stream-video-android-ui-core/src/main/res/values/dimens-legacy.xml new file mode 100644 index 0000000000..244d982672 --- /dev/null +++ b/stream-video-android-ui-core/src/main/res/values/dimens-legacy.xml @@ -0,0 +1,122 @@ + + + + + 86dp + 172dp + 4dp + 80dp + 64dp + 32dp + 64dp + 48dp + 20dp + 20dp + 65dp + 64dp + 44dp + 12dp + 0dp + 0dp + 8dp + 0dp + 8dp + 8dp + 18dp + 6dp + 96dp + 50dp + 45dp + 64dp + 8dp + 64dp + 56dp + 40dp + 56dp + 32dp + 16dp + 16dp + 200dp + 140dp + 16dp + 16dp + 10dp + 2dp + 2dp + 22dp + 2dp + 1dp + 24dp + 18dp + 2dp + 2dp + 1dp + 2dp + 2dp + 32dp + 8dp + 4dp + 8dp + 120dp + 4dp + 110dp + 125dp + 8dp + 4dp + 16dp + 16dp + 16dp + 16dp + 16dp + 35dp + 5dp + 40dp + 280dp + 12dp + 32dp + 24dp + 44dp + 84dp + 8dp + 4dp + 5dp + 20dp + 5dp + 22dp + 40dp + + 24sp + 34sp + 18sp + 23sp + 14sp + 12sp + 20sp + 10sp + 16sp + 10sp + 17sp + + 34sp + 24sp + 20sp + + 0.6 + 1.0 + 0.6 + + \ No newline at end of file diff --git a/stream-video-android-ui-core/src/main/res/values/dimens.xml b/stream-video-android-ui-core/src/main/res/values/dimens.xml index f6ea268a34..e6bf9e0328 100644 --- a/stream-video-android-ui-core/src/main/res/values/dimens.xml +++ b/stream-video-android-ui-core/src/main/res/values/dimens.xml @@ -1,6 +1,6 @@ + + 100dp + 84dp + 44dp + 32dp + 24dp + 16dp + 8dp + 4dp + 2dp - 86dp - 172dp - 4dp - 80dp - 64dp - 32dp - 64dp - 48dp - 20dp - 20dp - 65dp - 64dp - 44dp - 12dp - 0dp - 0dp - 8dp - 0dp - 8dp - 8dp - 18dp - 6dp - 96dp - 50dp - 45dp - 64dp - 8dp - 64dp - 56dp - 40dp - 56dp - 32dp - 16dp - 16dp - 200dp - 140dp - 16dp - 16dp - 10dp - 2dp - 2dp - 22dp - 2dp - 1dp - 24dp - 18dp - 2dp - 2dp - 1dp - 2dp - 2dp - 32dp - 8dp - 4dp - 8dp - 120dp - 4dp - 110dp - 125dp - 8dp - 4dp - 16dp - 16dp - 16dp - 16dp - 16dp - 35dp - 5dp - 40dp - 280dp - 12dp - 32dp - 24dp - 44dp - 84dp - 8dp - 4dp - 5dp - 20dp - 5dp - 22dp - 40dp + + @dimen/stream_video_generic_xl + @dimen/stream_video_generic_l + @dimen/stream_video_generic_m + @dimen/stream_video_generic_s - 24sp - 34sp - 18sp - 23sp - 14sp - 12sp - 20sp - 10sp - 16sp - 10sp - 17sp + + @dimen/stream_video_generic_xl + @dimen/stream_video_generic_l + @dimen/stream_video_generic_m + @dimen/stream_video_generic_s + @dimen/stream_video_generic_xs + @dimen/stream_video_generic_xxs - 34sp - 24sp - 20sp - - 0.6 - 1.0 - 0.6 + + @dimen/stream_video_generic_xxl + @dimen/stream_video_generic_xl + @dimen/stream_video_generic_l + @dimen/stream_video_spacing_s + @dimen/stream_video_spacing_m + @dimen/stream_video_spacing_s + @dimen/stream_video_spacing_m + @dimen/stream_video_spacing_s + + 93sp + 48sp + 24sp + 20sp + 16sp + 13sp + 43sp + 28sp + 24sp + 20sp + 16sp + 13sp \ No newline at end of file diff --git a/stream-video-android-ui-core/src/main/res/values/strings.xml b/stream-video-android-ui-core/src/main/res/values/strings.xml index 92e2e2c729..95bc06bf01 100644 --- a/stream-video-android-ui-core/src/main/res/values/strings.xml +++ b/stream-video-android-ui-core/src/main/res/values/strings.xml @@ -1,6 +1,6 @@