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