From 6785497062d23aa50829ef4d74571c344ce0d3dc Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Wed, 31 Jan 2024 16:29:50 +0900 Subject: [PATCH 01/13] Fix the intrudctions for the demo app (#1005) --- README.md | 10 +++++----- demo-app/README.md | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 8f1c1ab764..adb05d5567 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. diff --git a/demo-app/README.md b/demo-app/README.md index 4287ddd820..12b2b906d4 100644 --- a/demo-app/README.md +++ b/demo-app/README.md @@ -1,6 +1,6 @@ -# Dogfooding App +# Demo App -Dogfooding demonstrates Stream Video SDK for Android with modern Android tech stacks, such as Compose, Hilt, and Coroutines. +Demo demonstrates Stream Video SDK for Android with modern Android tech stacks, such as Compose, Hilt, and Coroutines. ## 📱 Previews @@ -12,11 +12,11 @@ Dogfooding demonstrates Stream Video SDK for Android with modern Android tech st ## Download -Dogfooding app is available on **[Google Play](https://play.google.com/store/apps/details?id=io.getstream.video.android)**. If you have any feedback on this, please [create an issue on GitHub](https://github.com/GetStream/stream-video-android/issues/new/choose). +Demo app is available on **[Google Play](https://play.google.com/store/apps/details?id=io.getstream.video.android)**. If you have any feedback on this, please [create an issue on GitHub](https://github.com/GetStream/stream-video-android/issues/new/choose). ## Build Setup -If you want to build and run the [dogfooding app](https://github.com/GetStream/stream-video-android/tree/develop/dogfooding) on your computer, you can follow the instructions below: +If you want to build and run the [demo app](https://github.com/GetStream/stream-video-android/tree/develop/demo-app) on your computer, you can follow the instructions below: 1. Get your Stream API KEY on the [Stream dashboard](https://dashboard.getstream.io?utm_source=Github&utm_medium=DevRel_GitHub_Repo_Jaewoong&utm_content=Developer&utm_campaign=Github_Sep2023_Jaewoong_StreamVideoSDK&utm_term=DevRelOss). @@ -43,19 +43,19 @@ If you want to build and run the [dogfooding app](https://github.com/GetStream/s -2. Next, create a file named **.env.properties** on the root project with the formats below: +7. Next, create a file named **.env.properties** on the root project with the formats below: ``` # Environment Variable for dogfooding app -DOGFOODING_RES_CONFIG_DEEPLINKING_HOST=pronto.getstream.io -DOGFOODING_RES_CONFIG_DEEPLINKING_HOST_LEGACY=stream-calls-dogfood.vercel.app +DOGFOODING_BUILD_CONFIG_API_KEY=YOUR_API_KEY +DOGFOODING_BUILD_CONFIG_BENCHMARK=true +DOGFOODING_RES_CONFIG_DEEPLINKING_HOST=stream-calls-dogfood.vercel.app DOGFOODING_RES_CONFIG_DEEPLINKING_PATH_PREFIX=/ -PRODUCTION_RES_CONFIG_DEEPLINKING_HOST=getstream.io -PRODUCTION_RES_CONFIG_DEEPLINKING_HOST_LEGACY= -PRODUCTION_RES_CONFIG_DEEPLINKING_PATH_PREFIX=/video/demos/ ``` -3. Finally, run the dogfooding project on your Android Studio. +Make sure that you properly copy-pasted the Stream API key to the `DOGFOODING_BUILD_CONFIG_API_KEY` property. + +8. Finally, run the demo project on your Android Studio. ## License From afae131bc8ad0ef4d7f261c63a3c32546f569264 Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Wed, 31 Jan 2024 16:36:40 +0900 Subject: [PATCH 02/13] Update years of the copyrights (#1006) --- README.md | 2 +- benchmark/build.gradle.kts | 2 +- benchmark/src/main/AndroidManifest.xml | 2 +- .../video/android/benchmark/BaselineProfileGenerator.kt | 2 +- .../main/kotlin/io/getstream/video/android/benchmark/Const.kt | 2 +- .../io/getstream/video/android/benchmark/DogfoodingScenarios.kt | 2 +- .../io/getstream/video/android/benchmark/StartupBenchmarks.kt | 2 +- .../main/kotlin/io/getstream/video/android/benchmark/Utils.kt | 2 +- demo-app/README.md | 2 +- demo-app/build.gradle.kts | 2 +- demo-app/lint-baseline.xml | 2 +- .../src/development/res/drawable/ic_launcher_background.xml | 2 +- demo-app/src/development/res/values/strings.xml | 2 +- demo-app/src/main/AndroidManifest.xml | 2 +- demo-app/src/main/kotlin/io/getstream/video/android/App.kt | 2 +- .../kotlin/io/getstream/video/android/DeeplinkingActivity.kt | 2 +- .../kotlin/io/getstream/video/android/DirectCallActivity.kt | 2 +- .../kotlin/io/getstream/video/android/IncomingCallActivity.kt | 2 +- .../src/main/kotlin/io/getstream/video/android/MainActivity.kt | 2 +- .../io/getstream/video/android/analytics/FirebaseEvents.kt | 2 +- .../video/android/data/repositories/GoogleAccountRepository.kt | 2 +- .../android/data/services/google/ListDirectoryPeopleResponse.kt | 2 +- .../video/android/data/services/stream/GetAuthDataResponse.kt | 2 +- .../video/android/data/services/stream/StreamService.kt | 2 +- .../src/main/kotlin/io/getstream/video/android/di/AppModule.kt | 2 +- .../kotlin/io/getstream/video/android/models/GoogleAccount.kt | 2 +- .../video/android/tooling/extensions/ContextExtensions.kt | 2 +- .../video/android/tooling/ui/ExceptionTraceActivity.kt | 2 +- .../getstream/video/android/tooling/ui/ExceptionTraceScreen.kt | 2 +- .../getstream/video/android/tooling/ui/StreamPrimaryButton.kt | 2 +- .../io/getstream/video/android/tooling/util/StreamFlavors.kt | 2 +- .../kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt | 2 +- .../io/getstream/video/android/ui/call/AvailableDeviceMenu.kt | 2 +- .../kotlin/io/getstream/video/android/ui/call/CallActivity.kt | 2 +- .../kotlin/io/getstream/video/android/ui/call/CallScreen.kt | 2 +- .../kotlin/io/getstream/video/android/ui/call/ChatDialog.kt | 2 +- .../kotlin/io/getstream/video/android/ui/call/ChatOverly.kt | 2 +- .../io/getstream/video/android/ui/call/CustomReactionContent.kt | 2 +- .../kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt | 2 +- .../kotlin/io/getstream/video/android/ui/call/ReactionsMenu.kt | 2 +- .../kotlin/io/getstream/video/android/ui/call/SettingsMenu.kt | 2 +- .../kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt | 2 +- .../io/getstream/video/android/ui/join/CallJoinViewModel.kt | 2 +- .../getstream/video/android/ui/join/barcode/BardcodeScanner.kt | 2 +- .../io/getstream/video/android/ui/lobby/CallLobbyScreen.kt | 2 +- .../io/getstream/video/android/ui/lobby/CallLobbyViewModel.kt | 2 +- .../kotlin/io/getstream/video/android/ui/login/GoogleSignIn.kt | 2 +- .../io/getstream/video/android/ui/login/GoogleSignInLauncher.kt | 2 +- .../kotlin/io/getstream/video/android/ui/login/LoginScreen.kt | 2 +- .../io/getstream/video/android/ui/login/LoginViewModel.kt | 2 +- .../getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt | 2 +- .../video/android/ui/outgoing/DirectCallJoinViewModel.kt | 2 +- .../main/kotlin/io/getstream/video/android/ui/theme/Colors.kt | 2 +- .../main/kotlin/io/getstream/video/android/ui/theme/LinkText.kt | 2 +- .../kotlin/io/getstream/video/android/ui/theme/StreamButton.kt | 2 +- .../io/getstream/video/android/ui/theme/StreamImageButton.kt | 2 +- .../video/android/util/BlurredBackgroundVideoFilter.kt | 2 +- .../kotlin/io/getstream/video/android/util/InAppUpdateHelper.kt | 2 +- .../kotlin/io/getstream/video/android/util/InstallReferrer.kt | 2 +- .../kotlin/io/getstream/video/android/util/NetworkMonitor.kt | 2 +- .../kotlin/io/getstream/video/android/util/SampleAudioFilter.kt | 2 +- .../kotlin/io/getstream/video/android/util/SampleVideoFilter.kt | 2 +- .../io/getstream/video/android/util/StreamVideoInitHelper.kt | 2 +- .../main/kotlin/io/getstream/video/android/util/UserHelper.kt | 2 +- .../kotlin/io/getstream/video/android/util/config/AppConfig.kt | 2 +- .../io/getstream/video/android/util/config/types/Flavor.kt | 2 +- .../video/android/util/config/types/StreamEnvironment.kt | 2 +- .../video/android/util/config/types/StreamRemoteConfig.kt | 2 +- .../video/android/util/config/types/SupportedLogins.kt | 2 +- demo-app/src/main/res/drawable/ic_blur_off.xml | 2 +- demo-app/src/main/res/drawable/ic_blur_on.xml | 2 +- demo-app/src/main/res/drawable/ic_default_avatar.xml | 2 +- demo-app/src/main/res/drawable/ic_launcher_background.xml | 2 +- demo-app/src/main/res/drawable/ic_launcher_foreground.xml | 2 +- demo-app/src/main/res/drawable/ic_mic.xml | 2 +- demo-app/src/main/res/drawable/ic_scan_qr.xml | 2 +- demo-app/src/main/res/drawable/ic_stream_video_meeting_logo.xml | 2 +- demo-app/src/main/res/layout/activity_main.xml | 2 +- demo-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- demo-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml | 2 +- demo-app/src/main/res/values/colors.xml | 2 +- demo-app/src/main/res/values/strings.xml | 2 +- demo-app/src/main/res/values/themes.xml | 2 +- demo-app/src/main/res/xml/remote_config_defaults.xml | 2 +- demo-app/src/production/res/values/strings.xml | 2 +- spotless/copyright.kt | 2 +- spotless/copyright.kts | 2 +- spotless/copyright.xml | 2 +- stream-video-android-core/build.gradle.kts | 2 +- stream-video-android-core/src/androidTest/AndroidManifest.xml | 2 +- .../kotlin/io/getstream/video/android/core/AndroidDeviceTest.kt | 2 +- .../kotlin/io/getstream/video/android/core/CallSwitchingTest.kt | 2 +- .../kotlin/io/getstream/video/android/core/DispatcherRule.kt | 2 +- .../io/getstream/video/android/core/IntegrationTestBase.kt | 2 +- .../kotlin/io/getstream/video/android/core/LivestreamTest.kt | 2 +- .../kotlin/io/getstream/video/android/core/MediaManagerTest.kt | 2 +- .../kotlin/io/getstream/video/android/core/ReconnectTest.kt | 2 +- .../kotlin/io/getstream/video/android/core/RingTest.kt | 2 +- .../kotlin/io/getstream/video/android/core/SupportTest.kt | 2 +- stream-video-android-core/src/main/AndroidManifest.xml | 2 +- .../src/main/kotlin/io/getstream/video/android/core/Call.kt | 2 +- .../kotlin/io/getstream/video/android/core/CallHealthMonitor.kt | 2 +- .../main/kotlin/io/getstream/video/android/core/CallState.kt | 2 +- .../main/kotlin/io/getstream/video/android/core/CallStats.kt | 2 +- .../kotlin/io/getstream/video/android/core/CallStatsReport.kt | 2 +- .../main/kotlin/io/getstream/video/android/core/ClientState.kt | 2 +- .../kotlin/io/getstream/video/android/core/EventSubscription.kt | 2 +- .../main/kotlin/io/getstream/video/android/core/MediaManager.kt | 2 +- .../main/kotlin/io/getstream/video/android/core/MemberState.kt | 2 +- .../kotlin/io/getstream/video/android/core/ParticipantState.kt | 2 +- .../main/kotlin/io/getstream/video/android/core/StreamVideo.kt | 2 +- .../io/getstream/video/android/core/StreamVideoBuilder.kt | 2 +- .../kotlin/io/getstream/video/android/core/StreamVideoConfig.kt | 2 +- .../kotlin/io/getstream/video/android/core/StreamVideoImpl.kt | 2 +- .../io/getstream/video/android/core/audio/AudioHandler.kt | 2 +- .../io/getstream/video/android/core/audio/StreamAudioDevice.kt | 2 +- .../kotlin/io/getstream/video/android/core/call/RtcSession.kt | 2 +- .../io/getstream/video/android/core/call/audio/AudioFilter.kt | 2 +- .../video/android/core/call/connection/StreamPeerConnection.kt | 2 +- .../android/core/call/connection/StreamPeerConnectionFactory.kt | 2 +- .../video/android/core/call/signal/socket/RTCEventMapper.kt | 2 +- .../io/getstream/video/android/core/call/state/CallAction.kt | 2 +- .../io/getstream/video/android/core/call/stats/RtcMapper.kt | 2 +- .../video/android/core/call/stats/model/RtcAudioSourceStats.kt | 2 +- .../video/android/core/call/stats/model/RtcCodecStats.kt | 2 +- .../android/core/call/stats/model/RtcIceCandidatePairStats.kt | 2 +- .../video/android/core/call/stats/model/RtcIceCandidateStats.kt | 2 +- .../core/call/stats/model/RtcInboundRtpAudioStreamStats.kt | 2 +- .../android/core/call/stats/model/RtcInboundRtpStreamStats.kt | 2 +- .../core/call/stats/model/RtcInboundRtpVideoStreamStats.kt | 2 +- .../video/android/core/call/stats/model/RtcMediaSourceStats.kt | 2 +- .../call/stats/model/RtcMediaStreamAudioTrackReceiverStats.kt | 2 +- .../call/stats/model/RtcMediaStreamAudioTrackSenderStats.kt | 2 +- .../core/call/stats/model/RtcMediaStreamAudioTrackStats.kt | 2 +- .../core/call/stats/model/RtcMediaStreamTrackReceiverStats.kt | 2 +- .../core/call/stats/model/RtcMediaStreamTrackSenderStats.kt | 2 +- .../android/core/call/stats/model/RtcMediaStreamTrackStats.kt | 2 +- .../call/stats/model/RtcMediaStreamVideoTrackReceiverStats.kt | 2 +- .../call/stats/model/RtcMediaStreamVideoTrackSenderStats.kt | 2 +- .../core/call/stats/model/RtcMediaStreamVideoTrackStats.kt | 2 +- .../core/call/stats/model/RtcOutboundRtpAudioStreamStats.kt | 2 +- .../android/core/call/stats/model/RtcOutboundRtpStreamStats.kt | 2 +- .../core/call/stats/model/RtcOutboundRtpVideoStreamStats.kt | 2 +- .../android/core/call/stats/model/RtcReceivedRtpStreamStats.kt | 2 +- .../call/stats/model/RtcRemoteInboundRtpAudioStreamStats.kt | 2 +- .../core/call/stats/model/RtcRemoteInboundRtpStreamStats.kt | 2 +- .../call/stats/model/RtcRemoteInboundRtpVideoStreamStats.kt | 2 +- .../call/stats/model/RtcRemoteOutboundRtpAudioStreamStats.kt | 2 +- .../core/call/stats/model/RtcRemoteOutboundRtpStreamStats.kt | 2 +- .../call/stats/model/RtcRemoteOutboundRtpVideoStreamStats.kt | 2 +- .../video/android/core/call/stats/model/RtcRtpStreamStats.kt | 2 +- .../android/core/call/stats/model/RtcSentRtpStreamStats.kt | 2 +- .../getstream/video/android/core/call/stats/model/RtcStats.kt | 2 +- .../video/android/core/call/stats/model/RtcStatsReport.kt | 2 +- .../video/android/core/call/stats/model/RtcVideoSourceStats.kt | 2 +- .../android/core/call/stats/model/discriminator/RtcMediaKind.kt | 2 +- .../stats/model/discriminator/RtcQualityLimitationReason.kt | 2 +- .../core/call/stats/model/discriminator/RtcReportType.kt | 2 +- .../android/core/call/utils/AudioValuePercentageNormaliser.kt | 2 +- .../getstream/video/android/core/call/utils/PeerConnection.kt | 2 +- .../io/getstream/video/android/core/call/utils/SDPUtils.kt | 2 +- .../video/android/core/call/utils/SoundInputProcessor.kt | 2 +- .../io/getstream/video/android/core/call/utils/Stringify.kt | 2 +- .../video/android/core/call/video/FilterVideoProcessor.kt | 2 +- .../io/getstream/video/android/core/call/video/VideoFilter.kt | 2 +- .../io/getstream/video/android/core/call/video/YuvFrame.kt | 2 +- .../video/android/core/dispatchers/DispatcherProvider.kt | 2 +- .../io/getstream/video/android/core/errors/DisconnectCause.kt | 2 +- .../kotlin/io/getstream/video/android/core/errors/Errors.kt | 2 +- .../io/getstream/video/android/core/errors/VideoErrorCode.kt | 2 +- .../io/getstream/video/android/core/events/SfuDataEvent.kt | 2 +- .../getstream/video/android/core/events/VideoEventListener.kt | 2 +- .../io/getstream/video/android/core/filter/FilterObject.kt | 2 +- .../io/getstream/video/android/core/filter/FilterObjectToMap.kt | 2 +- .../kotlin/io/getstream/video/android/core/filter/Filters.kt | 2 +- .../video/android/core/internal/InternalStreamVideoApi.kt | 2 +- .../video/android/core/internal/module/ConnectionModule.kt | 2 +- .../video/android/core/internal/network/NetworkStateProvider.kt | 2 +- .../getstream/video/android/core/lifecycle/LifecycleHandler.kt | 2 +- .../android/core/lifecycle/internal/StreamLifecycleObserver.kt | 2 +- .../io/getstream/video/android/core/logging/HttpLoggingLevel.kt | 2 +- .../io/getstream/video/android/core/logging/LoggingLevel.kt | 2 +- .../io/getstream/video/android/core/mapper/ReactionMapper.kt | 2 +- .../kotlin/io/getstream/video/android/core/model/CallData.kt | 2 +- .../io/getstream/video/android/core/model/CallEventType.kt | 2 +- .../io/getstream/video/android/core/model/CallRecordingData.kt | 2 +- .../kotlin/io/getstream/video/android/core/model/CallStatus.kt | 2 +- .../kotlin/io/getstream/video/android/core/model/EdgeData.kt | 2 +- .../io/getstream/video/android/core/model/IceCandidate.kt | 2 +- .../kotlin/io/getstream/video/android/core/model/IceServer.kt | 2 +- .../kotlin/io/getstream/video/android/core/model/Ingress.kt | 2 +- .../kotlin/io/getstream/video/android/core/model/MediaTrack.kt | 2 +- .../main/kotlin/io/getstream/video/android/core/model/Member.kt | 2 +- .../io/getstream/video/android/core/model/MuteUsersData.kt | 2 +- .../io/getstream/video/android/core/model/NetworkQuality.kt | 2 +- .../io/getstream/video/android/core/model/QueriedCalls.kt | 2 +- .../io/getstream/video/android/core/model/QueriedMembers.kt | 2 +- .../kotlin/io/getstream/video/android/core/model/Reaction.kt | 2 +- .../io/getstream/video/android/core/model/ReactionData.kt | 2 +- .../getstream/video/android/core/model/ScreenSharingSession.kt | 2 +- .../kotlin/io/getstream/video/android/core/model/SortData.kt | 2 +- .../io/getstream/video/android/core/model/StreamPeerType.kt | 2 +- .../video/android/core/model/UpdateUserPermissionsData.kt | 2 +- .../kotlin/io/getstream/video/android/core/model/VideoModel.kt | 2 +- .../video/android/core/model/VisibilityOnScreenState.kt | 2 +- .../android/core/notifications/DefaultNotificationHandler.kt | 2 +- .../video/android/core/notifications/NotificationConfig.kt | 2 +- .../video/android/core/notifications/NotificationHandler.kt | 2 +- .../core/notifications/internal/DefaultStreamIntentResolver.kt | 2 +- .../core/notifications/internal/DismissNotificationActivity.kt | 2 +- .../core/notifications/internal/NoOpNotificationHandler.kt | 2 +- .../core/notifications/internal/StreamNotificationManager.kt | 2 +- .../android/core/notifications/internal/VideoPushDelegate.kt | 2 +- .../internal/receivers/GenericCallActionBroadcastReceiver.kt | 2 +- .../internal/receivers/LeaveCallBroadcastReceiver.kt | 2 +- .../internal/receivers/RejectCallBroadcastReceiver.kt | 2 +- .../internal/receivers/StopScreenshareBroadcastReceiver.kt | 2 +- .../internal/receivers/ToggleCameraBroadcastReceiver.kt | 2 +- .../android/core/notifications/internal/service/CallService.kt | 2 +- .../core/notifications/internal/storage/DevicePreferences.kt | 2 +- .../internal/storage/DevicePreferencesSerializer.kt | 2 +- .../core/notifications/internal/storage/DeviceTokenStorage.kt | 2 +- .../video/android/core/permission/PermissionRequest.kt | 2 +- .../io/getstream/video/android/core/pinning/PinUpdates.kt | 2 +- .../video/android/core/screenshare/StreamScreenShareService.kt | 2 +- .../io/getstream/video/android/core/socket/CoordinatorSocket.kt | 2 +- .../io/getstream/video/android/core/socket/ErrorResponse.kt | 2 +- .../io/getstream/video/android/core/socket/PersistentSocket.kt | 2 +- .../kotlin/io/getstream/video/android/core/socket/SfuSocket.kt | 2 +- .../io/getstream/video/android/core/socket/SocketState.kt | 2 +- .../getstream/video/android/core/socket/internal/EventType.kt | 2 +- .../video/android/core/socket/internal/HealthMonitor.kt | 2 +- .../io/getstream/video/android/core/sorting/DefaultSortOrder.kt | 2 +- .../video/android/core/sorting/SortedParticipantsState.kt | 2 +- .../io/getstream/video/android/core/utils/AndroidUtils.kt | 2 +- .../io/getstream/video/android/core/utils/CallClientUtils.kt | 2 +- .../kotlin/io/getstream/video/android/core/utils/DebugInfo.kt | 2 +- .../kotlin/io/getstream/video/android/core/utils/DomainUtils.kt | 2 +- .../io/getstream/video/android/core/utils/LatencyResult.kt | 2 +- .../kotlin/io/getstream/video/android/core/utils/ListUtils.kt | 2 +- .../video/android/core/utils/RampValueUpAndDownHelper.kt | 2 +- .../kotlin/io/getstream/video/android/core/utils/SdpVersion.kt | 2 +- .../kotlin/io/getstream/video/android/core/utils/StateFlow.kt | 2 +- .../kotlin/io/getstream/video/android/core/utils/StringUtils.kt | 2 +- .../kotlin/io/getstream/video/android/core/utils/TokenUtils.kt | 2 +- .../kotlin/io/getstream/video/android/core/utils/UserUtils.kt | 2 +- .../video/android/datastore/delegate/StreamUserDataStore.kt | 2 +- .../video/android/datastore/model/StreamUserPreferences.kt | 2 +- .../video/android/datastore/serializer/EncryptedSerializer.kt | 2 +- .../video/android/datastore/serializer/UserSerializer.kt | 2 +- .../main/kotlin/io/getstream/video/android/model/Credentials.kt | 2 +- .../src/main/kotlin/io/getstream/video/android/model/Device.kt | 2 +- .../kotlin/io/getstream/video/android/model/StreamCallId.kt | 2 +- .../src/main/kotlin/io/getstream/video/android/model/User.kt | 2 +- .../kotlin/io/getstream/video/android/model/UserAudioLevel.kt | 2 +- .../main/kotlin/io/getstream/video/android/model/UserDevices.kt | 2 +- .../getstream/video/android/model/mapper/StreamCallCidMapper.kt | 2 +- .../src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt | 2 +- .../org/openapitools/client/infrastructure/BigDecimalAdapter.kt | 2 +- .../org/openapitools/client/infrastructure/BigIntegerAdapter.kt | 2 +- .../org/openapitools/client/infrastructure/ByteArrayAdapter.kt | 2 +- .../org/openapitools/client/infrastructure/CollectionFormats.kt | 2 +- .../org/openapitools/client/infrastructure/LocalDateAdapter.kt | 2 +- .../openapitools/client/infrastructure/LocalDateTimeAdapter.kt | 2 +- .../openapitools/client/infrastructure/OffsetDateTimeAdapter.kt | 2 +- .../kotlin/org/openapitools/client/infrastructure/Serializer.kt | 2 +- .../kotlin/org/openapitools/client/infrastructure/URIAdapter.kt | 2 +- .../org/openapitools/client/infrastructure/UUIDAdapter.kt | 2 +- .../src/main/kotlin/org/openapitools/client/models/APIError.kt | 2 +- .../kotlin/org/openapitools/client/models/AcceptCallResponse.kt | 2 +- .../main/kotlin/org/openapitools/client/models/AudioSettings.kt | 2 +- .../org/openapitools/client/models/AudioSettingsRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/BackstageSettings.kt | 2 +- .../org/openapitools/client/models/BackstageSettingsRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/BlockUserRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/BlockUserResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/BlockedUserEvent.kt | 2 +- .../kotlin/org/openapitools/client/models/BroadcastSettings.kt | 2 +- .../org/openapitools/client/models/BroadcastSettingsRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/CallAcceptedEvent.kt | 2 +- .../openapitools/client/models/CallBroadcastingStartedEvent.kt | 2 +- .../openapitools/client/models/CallBroadcastingStoppedEvent.kt | 2 +- .../kotlin/org/openapitools/client/models/CallCreatedEvent.kt | 2 +- .../kotlin/org/openapitools/client/models/CallEndedEvent.kt | 2 +- .../org/openapitools/client/models/CallIngressResponse.kt | 2 +- .../org/openapitools/client/models/CallLiveStartedEvent.kt | 2 +- .../org/openapitools/client/models/CallMemberAddedEvent.kt | 2 +- .../org/openapitools/client/models/CallMemberRemovedEvent.kt | 2 +- .../org/openapitools/client/models/CallMemberUpdatedEvent.kt | 2 +- .../client/models/CallMemberUpdatedPermissionEvent.kt | 2 +- .../org/openapitools/client/models/CallNotificationEvent.kt | 2 +- .../org/openapitools/client/models/CallParticipantResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/CallReactionEvent.kt | 2 +- .../main/kotlin/org/openapitools/client/models/CallRecording.kt | 2 +- .../org/openapitools/client/models/CallRecordingFailedEvent.kt | 2 +- .../org/openapitools/client/models/CallRecordingReadyEvent.kt | 2 +- .../org/openapitools/client/models/CallRecordingStartedEvent.kt | 2 +- .../org/openapitools/client/models/CallRecordingStoppedEvent.kt | 2 +- .../kotlin/org/openapitools/client/models/CallRejectedEvent.kt | 2 +- .../main/kotlin/org/openapitools/client/models/CallRequest.kt | 2 +- .../main/kotlin/org/openapitools/client/models/CallResponse.kt | 2 +- .../main/kotlin/org/openapitools/client/models/CallRingEvent.kt | 2 +- .../org/openapitools/client/models/CallSessionEndedEvent.kt | 2 +- .../client/models/CallSessionParticipantJoinedEvent.kt | 2 +- .../client/models/CallSessionParticipantLeftEvent.kt | 2 +- .../org/openapitools/client/models/CallSessionResponse.kt | 2 +- .../org/openapitools/client/models/CallSessionStartedEvent.kt | 2 +- .../org/openapitools/client/models/CallSettingsRequest.kt | 2 +- .../org/openapitools/client/models/CallSettingsResponse.kt | 2 +- .../org/openapitools/client/models/CallStateResponseFields.kt | 2 +- .../kotlin/org/openapitools/client/models/CallUpdatedEvent.kt | 2 +- .../main/kotlin/org/openapitools/client/models/CallUserMuted.kt | 2 +- .../org/openapitools/client/models/ConnectUserDetailsRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/ConnectedEvent.kt | 2 +- .../org/openapitools/client/models/ConnectionErrorEvent.kt | 2 +- .../org/openapitools/client/models/CreateDeviceRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/CreateGuestRequest.kt | 2 +- .../org/openapitools/client/models/CreateGuestResponse.kt | 2 +- .../main/kotlin/org/openapitools/client/models/Credentials.kt | 2 +- .../kotlin/org/openapitools/client/models/CustomVideoEvent.kt | 2 +- .../src/main/kotlin/org/openapitools/client/models/Device.kt | 2 +- .../main/kotlin/org/openapitools/client/models/EdgeResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/EgressHLSResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/EgressRTMPResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/EgressResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/EndCallResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/GeofenceSettings.kt | 2 +- .../org/openapitools/client/models/GeofenceSettingsRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/GetCallResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/GetEdgesResponse.kt | 2 +- .../org/openapitools/client/models/GetOrCreateCallRequest.kt | 2 +- .../org/openapitools/client/models/GetOrCreateCallResponse.kt | 2 +- .../main/kotlin/org/openapitools/client/models/GoLiveRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/GoLiveResponse.kt | 2 +- .../main/kotlin/org/openapitools/client/models/HLSSettings.kt | 2 +- .../kotlin/org/openapitools/client/models/HLSSettingsRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/HealthCheckEvent.kt | 2 +- .../src/main/kotlin/org/openapitools/client/models/ICEServer.kt | 2 +- .../kotlin/org/openapitools/client/models/JoinCallRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/JoinCallResponse.kt | 2 +- .../org/openapitools/client/models/ListDevicesResponse.kt | 2 +- .../org/openapitools/client/models/ListRecordingsResponse.kt | 2 +- .../main/kotlin/org/openapitools/client/models/MemberRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/MemberResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/MuteUsersRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/MuteUsersResponse.kt | 2 +- .../main/kotlin/org/openapitools/client/models/OwnCapability.kt | 2 +- .../kotlin/org/openapitools/client/models/OwnUserResponse.kt | 2 +- .../org/openapitools/client/models/PermissionRequestEvent.kt | 2 +- .../main/kotlin/org/openapitools/client/models/PinRequest.kt | 2 +- .../main/kotlin/org/openapitools/client/models/PinResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/QueryCallsRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/QueryCallsResponse.kt | 2 +- .../org/openapitools/client/models/QueryMembersRequest.kt | 2 +- .../org/openapitools/client/models/QueryMembersResponse.kt | 2 +- .../main/kotlin/org/openapitools/client/models/RTMPIngress.kt | 2 +- .../kotlin/org/openapitools/client/models/ReactionResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/RecordSettings.kt | 2 +- .../org/openapitools/client/models/RecordSettingsRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/RejectCallResponse.kt | 2 +- .../org/openapitools/client/models/RequestPermissionRequest.kt | 2 +- .../org/openapitools/client/models/RequestPermissionResponse.kt | 2 +- .../src/main/kotlin/org/openapitools/client/models/Response.kt | 2 +- .../main/kotlin/org/openapitools/client/models/RingSettings.kt | 2 +- .../org/openapitools/client/models/RingSettingsRequest.kt | 2 +- .../main/kotlin/org/openapitools/client/models/SFUResponse.kt | 2 +- .../org/openapitools/client/models/ScreensharingSettings.kt | 2 +- .../openapitools/client/models/ScreensharingSettingsRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/SendEventRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/SendEventResponse.kt | 2 +- .../org/openapitools/client/models/SendReactionRequest.kt | 2 +- .../org/openapitools/client/models/SendReactionResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/SortParamRequest.kt | 2 +- .../org/openapitools/client/models/StartBroadcastingResponse.kt | 2 +- .../org/openapitools/client/models/StartRecordingResponse.kt | 2 +- .../openapitools/client/models/StartTranscriptionResponse.kt | 2 +- .../org/openapitools/client/models/StopBroadcastingResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/StopLiveResponse.kt | 2 +- .../org/openapitools/client/models/StopRecordingResponse.kt | 2 +- .../org/openapitools/client/models/StopTranscriptionResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/TargetResolution.kt | 2 +- .../org/openapitools/client/models/TargetResolutionRequest.kt | 2 +- .../org/openapitools/client/models/TranscriptionSettings.kt | 2 +- .../openapitools/client/models/TranscriptionSettingsRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/UnblockUserRequest.kt | 2 +- .../org/openapitools/client/models/UnblockUserResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/UnblockedUserEvent.kt | 2 +- .../main/kotlin/org/openapitools/client/models/UnpinRequest.kt | 2 +- .../main/kotlin/org/openapitools/client/models/UnpinResponse.kt | 2 +- .../org/openapitools/client/models/UpdateCallMembersRequest.kt | 2 +- .../org/openapitools/client/models/UpdateCallMembersResponse.kt | 2 +- .../kotlin/org/openapitools/client/models/UpdateCallRequest.kt | 2 +- .../kotlin/org/openapitools/client/models/UpdateCallResponse.kt | 2 +- .../openapitools/client/models/UpdateUserPermissionsRequest.kt | 2 +- .../openapitools/client/models/UpdateUserPermissionsResponse.kt | 2 +- .../openapitools/client/models/UpdatedCallPermissionsEvent.kt | 2 +- .../main/kotlin/org/openapitools/client/models/UserRequest.kt | 2 +- .../main/kotlin/org/openapitools/client/models/UserResponse.kt | 2 +- .../main/kotlin/org/openapitools/client/models/VideoEvent.kt | 2 +- .../main/kotlin/org/openapitools/client/models/VideoSettings.kt | 2 +- .../org/openapitools/client/models/VideoSettingsRequest.kt | 2 +- .../org/openapitools/client/models/WSAuthMessageRequest.kt | 2 +- .../main/kotlin/org/openapitools/client/models/WSCallEvent.kt | 2 +- .../main/kotlin/org/openapitools/client/models/WSClientEvent.kt | 2 +- .../src/main/res/drawable/stream_video_ic_call.xml | 2 +- .../main/res/drawable/stream_video_ic_cancel_screenshare.xml | 2 +- .../src/main/res/drawable/stream_video_ic_screenshare.xml | 2 +- stream-video-android-core/src/main/res/values/ids.xml | 2 +- stream-video-android-core/src/main/res/values/strings.xml | 2 +- .../test/kotlin/io/getstream/video/android/core/CallCrudTest.kt | 2 +- .../kotlin/io/getstream/video/android/core/CallStateTest.kt | 2 +- .../kotlin/io/getstream/video/android/core/CameraManagerTest.kt | 2 +- .../kotlin/io/getstream/video/android/core/ClientAndAuthTest.kt | 2 +- .../test/kotlin/io/getstream/video/android/core/EventTest.kt | 2 +- .../io/getstream/video/android/core/MicrophoneManagerTest.kt | 2 +- .../src/test/kotlin/io/getstream/video/android/core/SdpTest.kt | 2 +- .../test/kotlin/io/getstream/video/android/core/SocketTest.kt | 2 +- .../io/getstream/video/android/core/base/DispatcherRule.kt | 2 +- .../io/getstream/video/android/core/base/IntegrationTestBase.kt | 2 +- .../getstream/video/android/core/base/IntegrationTestHelper.kt | 2 +- .../io/getstream/video/android/core/base/StreamTestLogger.kt | 2 +- .../kotlin/io/getstream/video/android/core/base/TestBase.kt | 2 +- .../kotlin/io/getstream/video/android/core/rtc/JoinCallTest.kt | 2 +- .../io/getstream/video/android/core/rtc/RtcSessionTest.kt | 2 +- .../io/getstream/video/android/core/stories/AudioRoomTest.kt | 2 +- .../io/getstream/video/android/core/stories/ModerationTest.kt | 2 +- .../kotlin/io/getstream/video/android/core/stories/RingTest.kt | 2 +- .../video/android/core/utils/RampValueUpAndDownHelperTest.kt | 2 +- .../io/getstream/video/android/core/utils/TokenUtilsTest.kt | 2 +- stream-video-android-previewdata/build.gradle.kts | 2 +- stream-video-android-previewdata/src/main/AndroidManifest.xml | 2 +- .../io/getstream/video/android/mock/StreamPreviewDataUtils.kt | 2 +- stream-video-android-ui-compose/build.gradle.kts | 2 +- stream-video-android-ui-compose/src/main/AndroidManifest.xml | 2 +- .../video/android/compose/lifecycle/MediaPiPLifecycle.kt | 2 +- .../video/android/compose/permission/CallPermissions.kt | 2 +- .../video/android/compose/permission/SinglePermission.kt | 2 +- .../video/android/compose/permission/VideoPermissionsState.kt | 2 +- .../io/getstream/video/android/compose/pip/PictureInPicture.kt | 2 +- .../compose/state/ui/internal/CallParticipantInfoMode.kt | 2 +- .../compose/state/ui/internal/CallParticipantsInfoOption.kt | 2 +- .../compose/state/ui/participants/ParticipantInfoAction.kt | 2 +- .../io/getstream/video/android/compose/theme/StreamColors.kt | 2 +- .../io/getstream/video/android/compose/theme/StreamDimens.kt | 2 +- .../getstream/video/android/compose/theme/StreamRippleTheme.kt | 2 +- .../io/getstream/video/android/compose/theme/StreamShapes.kt | 2 +- .../getstream/video/android/compose/theme/StreamTypography.kt | 2 +- .../io/getstream/video/android/compose/theme/VideoTheme.kt | 2 +- .../video/android/compose/ui/components/audio/AudioAppBar.kt | 2 +- .../android/compose/ui/components/audio/AudioControlActions.kt | 2 +- .../compose/ui/components/audio/AudioParticipantsGrid.kt | 2 +- .../android/compose/ui/components/audio/AudioRendererStyle.kt | 2 +- .../android/compose/ui/components/audio/AudioRoomContent.kt | 2 +- .../android/compose/ui/components/audio/ParticipantAudio.kt | 2 +- .../video/android/compose/ui/components/avatar/Avatar.kt | 2 +- .../android/compose/ui/components/avatar/InitialsAvatar.kt | 2 +- .../compose/ui/components/avatar/LocalAvatarPreviewProvider.kt | 2 +- .../android/compose/ui/components/avatar/OnlineIndicator.kt | 2 +- .../compose/ui/components/avatar/OnlineIndicatorAlignment.kt | 2 +- .../video/android/compose/ui/components/avatar/UserAvatar.kt | 2 +- .../compose/ui/components/avatar/UserAvatarBackground.kt | 2 +- .../android/compose/ui/components/background/CallBackground.kt | 2 +- .../video/android/compose/ui/components/call/CallAppBar.kt | 2 +- .../compose/ui/components/call/activecall/CallContent.kt | 2 +- .../ui/components/call/activecall/internal/InviteUsersDialog.kt | 2 +- .../compose/ui/components/call/controls/ControlActions.kt | 2 +- .../ui/components/call/controls/actions/AcceptCallAction.kt | 2 +- .../ui/components/call/controls/actions/AudioControlsActions.kt | 2 +- .../call/controls/actions/CallControlActionBackground.kt | 2 +- .../ui/components/call/controls/actions/CallControlsActions.kt | 2 +- .../ui/components/call/controls/actions/CancelCallAction.kt | 2 +- .../ui/components/call/controls/actions/ChatDialogAction.kt | 2 +- .../ui/components/call/controls/actions/DeclineCallAction.kt | 2 +- .../ui/components/call/controls/actions/FlipCameraAction.kt | 2 +- .../components/call/controls/actions/LandscapeControlActions.kt | 2 +- .../ui/components/call/controls/actions/LeaveCallAction.kt | 2 +- .../ui/components/call/controls/actions/ReactionAction.kt | 2 +- .../components/call/controls/actions/RegularControlActions.kt | 2 +- .../ui/components/call/controls/actions/SettingsAction.kt | 2 +- .../ui/components/call/controls/actions/ToggleCameraAction.kt | 2 +- .../components/call/controls/actions/ToggleMicrophoneAction.kt | 2 +- .../call/controls/actions/ToggleSpeakerphoneAction.kt | 2 +- .../ui/components/call/diagnostics/CallDiagnosticsContent.kt | 2 +- .../video/android/compose/ui/components/call/lobby/CallLobby.kt | 2 +- .../compose/ui/components/call/lobby/LobbyControlsActions.kt | 2 +- .../compose/ui/components/call/pinning/ParticipantActions.kt | 2 +- .../ui/components/call/renderer/FloatingParticipantVideo.kt | 2 +- .../compose/ui/components/call/renderer/ParticipantVideo.kt | 2 +- .../compose/ui/components/call/renderer/ParticipantsLayout.kt | 2 +- .../ui/components/call/renderer/ParticipantsRegularGrid.kt | 2 +- .../ui/components/call/renderer/ParticipantsScreenSharing.kt | 2 +- .../ui/components/call/renderer/ParticipantsSpotlight.kt | 2 +- .../compose/ui/components/call/renderer/VideoRendererStyle.kt | 2 +- .../compose/ui/components/call/renderer/internal/Common.kt | 2 +- .../components/call/renderer/internal/InternalLocalComposite.kt | 2 +- .../renderer/internal/LandscapeScreenSharingVideoRenderer.kt | 2 +- .../components/call/renderer/internal/LandscapeVideoRenderer.kt | 2 +- .../call/renderer/internal/LazyColumnVideoRenderer.kt | 2 +- .../components/call/renderer/internal/LazyRowVideoRenderer.kt | 2 +- .../call/renderer/internal/OrientationVideoRenderer.kt | 2 +- .../renderer/internal/PortraitScreenSharingVideoRenderer.kt | 2 +- .../components/call/renderer/internal/PortraitVideoRenderer.kt | 2 +- .../ui/components/call/renderer/internal/ScreenShareTooltip.kt | 2 +- .../call/renderer/internal/ScreenShareVideoRenderer.kt | 2 +- .../components/call/renderer/internal/SpotlightVideorenderer.kt | 2 +- .../compose/ui/components/call/ringing/RingingCallContent.kt | 2 +- .../components/call/ringing/incomingcall/IncomingCallContent.kt | 2 +- .../call/ringing/incomingcall/IncomingCallControls.kt | 2 +- .../components/call/ringing/incomingcall/IncomingCallDetails.kt | 2 +- .../components/call/ringing/outgoingcall/OutgoingCallContent.kt | 2 +- .../call/ringing/outgoingcall/OutgoingCallControls.kt | 2 +- .../components/call/ringing/outgoingcall/OutgoingCallDetails.kt | 2 +- .../compose/ui/components/connection/NetworkQualityIndicator.kt | 2 +- .../compose/ui/components/connection/internal/ConnectionBars.kt | 2 +- .../compose/ui/components/indicator/AudioVolumeIndicator.kt | 2 +- .../android/compose/ui/components/indicator/GenericIndicator.kt | 2 +- .../compose/ui/components/indicator/MicrophoneIndicator.kt | 2 +- .../android/compose/ui/components/indicator/SoundIndicator.kt | 2 +- .../compose/ui/components/livestream/LivestreamBackStage.kt | 2 +- .../compose/ui/components/livestream/LivestreamPlayer.kt | 2 +- .../compose/ui/components/livestream/LivestreamPlayerOverlay.kt | 2 +- .../compose/ui/components/livestream/LivestreamRenderer.kt | 2 +- .../ui/components/participants/CallParticipantsInfoMenu.kt | 2 +- .../compose/ui/components/participants/ParticipantIndicator.kt | 2 +- .../participants/internal/CallParticipantListAppBar.kt | 2 +- .../participants/internal/CallParticipantsInfoActions.kt | 2 +- .../ui/components/participants/internal/CallParticipantsList.kt | 2 +- .../ui/components/participants/internal/InviteUserList.kt | 2 +- .../ui/components/participants/internal/ParticipantAvatars.kt | 2 +- .../components/participants/internal/ParticipantInformation.kt | 2 +- .../participants/internal/SelectedCallParticipantOptions.kt | 2 +- .../compose/ui/components/plugins/BlurTransformationPlugin.kt | 2 +- .../compose/ui/components/plugins/RememberBlurPainter.kt | 2 +- .../compose/ui/components/plugins/TransformationPainter.kt | 2 +- .../video/android/compose/ui/components/video/VideoRenderer.kt | 2 +- .../android/compose/ui/components/video/VideoRendererView.kt | 2 +- .../android/compose/ui/components/video/VideoScalingType.kt | 2 +- .../video/android/compose/ui/extensions/ModifierExtensions.kt | 2 +- .../io/getstream/video/android/compose/utils/ImageUtils.kt | 2 +- .../io/getstream/video/android/compose/utils/Resources.kt | 2 +- .../kotlin/io/getstream/video/android/compose/AudioRoomTest.kt | 2 +- .../kotlin/io/getstream/video/android/compose/AvatarTest.kt | 2 +- .../io/getstream/video/android/compose/CallBackgroundTest.kt | 2 +- .../video/android/compose/CallComponentsLandscapeTest.kt | 2 +- .../video/android/compose/CallComponentsPortraitTest.kt | 2 +- .../io/getstream/video/android/compose/CallContentTest.kt | 2 +- .../io/getstream/video/android/compose/CallControlsTest.kt | 2 +- .../kotlin/io/getstream/video/android/compose/CallLobbyTest.kt | 2 +- .../kotlin/io/getstream/video/android/compose/IndicatorsTest.kt | 2 +- .../kotlin/io/getstream/video/android/compose/LivestreamTest.kt | 2 +- .../getstream/video/android/compose/ParticipantLandscapeTest.kt | 2 +- .../getstream/video/android/compose/ParticipantsPortraitTest.kt | 2 +- .../io/getstream/video/android/compose/base/BaseComposeTest.kt | 2 +- stream-video-android-ui-core/build.gradle.kts | 2 +- stream-video-android-ui-core/src/main/AndroidManifest.xml | 2 +- .../getstream/video/android/ui/common/AbstractCallActivity.kt | 2 +- .../ui/common/notification/AbstractNotificationActivity.kt | 2 +- .../video/android/ui/common/permission/PermissionManager.kt | 2 +- .../android/ui/common/permission/PermissionManagerProvider.kt | 2 +- .../ui/common/renderer/StreamVideoTextureViewRenderer.kt | 2 +- .../io/getstream/video/android/ui/common/util/ColorUtils.kt | 2 +- .../getstream/video/android/ui/common/util/ParticipantsText.kt | 2 +- .../io/getstream/video/android/ui/common/util/Resources.kt | 2 +- .../io/getstream/video/android/ui/common/util/StateFlow.kt | 2 +- .../video/android/ui/common/view/ParticipantContentView.kt | 2 +- .../video/android/ui/common/view/ParticipantItemView.kt | 2 +- .../src/main/res/drawable/stream_video_ic_arrow_back.xml | 2 +- .../src/main/res/drawable/stream_video_ic_call.xml | 2 +- .../src/main/res/drawable/stream_video_ic_call_end.xml | 2 +- .../src/main/res/drawable/stream_video_ic_camera_flip.xml | 2 +- .../src/main/res/drawable/stream_video_ic_close.xml | 2 +- .../src/main/res/drawable/stream_video_ic_fullscreen.xml | 2 +- .../src/main/res/drawable/stream_video_ic_fullscreen_exit.xml | 2 +- .../src/main/res/drawable/stream_video_ic_landscape_mode.xml | 2 +- .../src/main/res/drawable/stream_video_ic_leave.xml | 2 +- .../src/main/res/drawable/stream_video_ic_live.xml | 2 +- .../src/main/res/drawable/stream_video_ic_message.xml | 2 +- .../src/main/res/drawable/stream_video_ic_mic_off.xml | 2 +- .../src/main/res/drawable/stream_video_ic_mic_on.xml | 2 +- .../src/main/res/drawable/stream_video_ic_options.xml | 2 +- .../src/main/res/drawable/stream_video_ic_participants.xml | 2 +- .../src/main/res/drawable/stream_video_ic_play.xml | 2 +- .../src/main/res/drawable/stream_video_ic_portrait_mode.xml | 2 +- .../src/main/res/drawable/stream_video_ic_preview_avatar.xml | 2 +- .../src/main/res/drawable/stream_video_ic_reaction.xml | 2 +- .../src/main/res/drawable/stream_video_ic_screensharing.xml | 2 +- .../src/main/res/drawable/stream_video_ic_selected.xml | 2 +- .../src/main/res/drawable/stream_video_ic_speaker_off.xml | 2 +- .../src/main/res/drawable/stream_video_ic_speaker_on.xml | 2 +- .../src/main/res/drawable/stream_video_ic_videocam_off.xml | 2 +- .../src/main/res/drawable/stream_video_ic_videocam_on.xml | 2 +- .../src/main/res/layout/stream_video_content_participant.xml | 2 +- .../src/main/res/values-night/colors.xml | 2 +- stream-video-android-ui-core/src/main/res/values/colors.xml | 2 +- stream-video-android-ui-core/src/main/res/values/dimens.xml | 2 +- stream-video-android-ui-core/src/main/res/values/strings.xml | 2 +- stream-video-android-ui-xml/build.gradle.kts | 2 +- stream-video-android-ui-xml/lint-baseline.xml | 2 +- stream-video-android-ui-xml/src/main/AndroidManifest.xml | 2 +- .../src/main/kotlin/io/getstream/video/android/xml/VideoUI.kt | 2 +- .../io/getstream/video/android/xml/binding/CallAppBarBinding.kt | 2 +- .../video/android/xml/binding/CallContainterViewBinding.kt | 2 +- .../video/android/xml/binding/CallControlsViewBinding.kt | 2 +- .../video/android/xml/binding/CallParticipantGridViewBinding.kt | 2 +- .../video/android/xml/binding/CallParticipantListViewBinding.kt | 2 +- .../video/android/xml/binding/CallParticipantViewBinding.kt | 2 +- .../io/getstream/video/android/xml/binding/CallViewBinding.kt | 2 +- .../video/android/xml/binding/FloatingParticipantViewBinding.kt | 2 +- .../video/android/xml/binding/IncomingCallViewBinding.kt | 2 +- .../video/android/xml/binding/OutgoingCallViewBinding.kt | 2 +- .../video/android/xml/binding/PictureInPictureViewBinding.kt | 2 +- .../video/android/xml/binding/ScreenShareViewBinding.kt | 2 +- .../kotlin/io/getstream/video/android/xml/font/TextStyle.kt | 2 +- .../kotlin/io/getstream/video/android/xml/font/VideoFonts.kt | 2 +- .../io/getstream/video/android/xml/font/VideoFontsImpl.kt | 2 +- .../kotlin/io/getstream/video/android/xml/font/VideoStyle.kt | 2 +- .../getstream/video/android/xml/imageloading/CoilDisposable.kt | 2 +- .../video/android/xml/imageloading/CoilStreamImageLoader.kt | 2 +- .../android/xml/imageloading/DefaultStreamCoilImageLoader.kt | 2 +- .../video/android/xml/imageloading/ImageHeadersProvider.kt | 2 +- .../android/xml/imageloading/RoundedCornersTransformation.kt | 2 +- .../io/getstream/video/android/xml/imageloading/StreamCoil.kt | 2 +- .../video/android/xml/imageloading/StreamImageLoader.kt | 2 +- .../video/android/xml/imageloading/StreamImageLoaderFactory.kt | 2 +- .../video/android/xml/initializer/VideoUIInitializer.kt | 2 +- .../io/getstream/video/android/xml/state/CallDeviceState.kt | 2 +- .../kotlin/io/getstream/video/android/xml/utils/Disposable.kt | 2 +- .../io/getstream/video/android/xml/utils/LazyVarDelegate.kt | 2 +- .../video/android/xml/utils/OrientationChangeListener.kt | 2 +- .../video/android/xml/utils/extensions/ConstraintLayout.kt | 2 +- .../io/getstream/video/android/xml/utils/extensions/Context.kt | 2 +- .../getstream/video/android/xml/utils/extensions/ImageUtils.kt | 2 +- .../io/getstream/video/android/xml/utils/extensions/Int.kt | 2 +- .../getstream/video/android/xml/utils/extensions/TypedArray.kt | 2 +- .../io/getstream/video/android/xml/utils/extensions/View.kt | 2 +- .../getstream/video/android/xml/utils/extensions/ViewGroup.kt | 2 +- .../io/getstream/video/android/xml/viewmodel/CallViewModel.kt | 2 +- .../video/android/xml/viewmodel/CallViewModelFactory.kt | 2 +- .../video/android/xml/viewmodel/CallViewModelFactoryProvider.kt | 2 +- .../video/android/xml/widget/appbar/CallAppBarContent.kt | 2 +- .../video/android/xml/widget/appbar/CallAppBarStyle.kt | 2 +- .../getstream/video/android/xml/widget/appbar/CallAppBarView.kt | 2 +- .../widget/appbar/internal/DefaultCallAppBarCenterContent.kt | 2 +- .../widget/appbar/internal/DefaultCallAppBarLeadingContent.kt | 2 +- .../widget/appbar/internal/DefaultCallAppBarTrailingContent.kt | 2 +- .../io/getstream/video/android/xml/widget/avatar/AvatarShape.kt | 2 +- .../io/getstream/video/android/xml/widget/avatar/AvatarStyle.kt | 2 +- .../io/getstream/video/android/xml/widget/avatar/AvatarView.kt | 2 +- .../xml/widget/avatar/internal/AvatarPlaceholderDrawable.kt | 2 +- .../io/getstream/video/android/xml/widget/call/CallView.kt | 2 +- .../io/getstream/video/android/xml/widget/call/CallViewStyle.kt | 2 +- .../android/xml/widget/callcontainer/CallContainerStyle.kt | 2 +- .../video/android/xml/widget/callcontainer/CallContainerView.kt | 2 +- .../video/android/xml/widget/calldetails/CallBackgroundView.kt | 2 +- .../video/android/xml/widget/calldetails/CallDetailsStyle.kt | 2 +- .../video/android/xml/widget/calldetails/CallDetailsView.kt | 2 +- .../video/android/xml/widget/control/CallControlItem.kt | 2 +- .../video/android/xml/widget/control/CallControlsStyle.kt | 2 +- .../video/android/xml/widget/control/CallControlsView.kt | 2 +- .../video/android/xml/widget/control/ControlButtonStyle.kt | 2 +- .../video/android/xml/widget/control/ControlButtonView.kt | 2 +- .../video/android/xml/widget/incoming/IncomingCallStyle.kt | 2 +- .../video/android/xml/widget/incoming/IncomingCallView.kt | 2 +- .../video/android/xml/widget/outgoing/OutgoingCallStyle.kt | 2 +- .../video/android/xml/widget/outgoing/OutgoingCallView.kt | 2 +- .../xml/widget/participant/CallParticipantLabelAlignment.kt | 2 +- .../android/xml/widget/participant/CallParticipantStyle.kt | 2 +- .../video/android/xml/widget/participant/CallParticipantView.kt | 2 +- .../android/xml/widget/participant/FloatingParticipantView.kt | 2 +- .../android/xml/widget/participant/PictureInPictureStyle.kt | 2 +- .../android/xml/widget/participant/PictureInPictureView.kt | 2 +- .../video/android/xml/widget/participant/RendererInitializer.kt | 2 +- .../xml/widget/participant/internal/CallParticipantsGridView.kt | 2 +- .../xml/widget/participant/internal/CallParticipantsListView.kt | 2 +- .../video/android/xml/widget/renderer/VideoRenderer.kt | 2 +- .../video/android/xml/widget/screenshare/ScreenShareStyle.kt | 2 +- .../video/android/xml/widget/screenshare/ScreenShareView.kt | 2 +- .../video/android/xml/widget/transformer/TransformStyle.kt | 2 +- .../io/getstream/video/android/xml/widget/view/CallCardView.kt | 2 +- .../video/android/xml/widget/view/CallConstraintLayout.kt | 2 +- .../io/getstream/video/android/xml/widget/view/JobHolder.kt | 2 +- .../res/drawable/stream_video_bg_active_call_participant.xml | 2 +- .../main/res/drawable/stream_video_bg_call_control_option.xml | 2 +- .../src/main/res/drawable/stream_video_bg_call_option.xml | 2 +- .../main/res/drawable/stream_video_bg_call_participant_name.xml | 2 +- .../src/main/res/drawable/stream_video_rect_active_speaker.xml | 2 +- .../src/main/res/drawable/stream_video_rect_controls.xml | 2 +- .../main/res/drawable/stream_video_rect_controls_landscape.xml | 2 +- .../src/main/res/layout/stream_video_activity_call.xml | 2 +- .../src/main/res/layout/stream_video_item_participant.xml | 2 +- .../src/main/res/layout/stream_video_view_call_app_bar.xml | 2 +- .../src/main/res/layout/stream_video_view_call_details.xml | 2 +- .../src/main/res/layout/stream_video_view_call_participant.xml | 2 +- .../src/main/res/layout/stream_video_view_control_button.xml | 2 +- .../main/res/layout/stream_video_view_floating_participant.xml | 2 +- .../src/main/res/layout/stream_video_view_incoming_call.xml | 2 +- .../src/main/res/layout/stream_video_view_outgoing_call.xml | 2 +- .../src/main/res/layout/stream_video_view_screen_share.xml | 2 +- .../src/main/res/menu/stream_video_call_menu.xml | 2 +- .../src/main/res/values/stream_video_attrs.xml | 2 +- .../src/main/res/values/stream_video_attrs_avatar_view.xml | 2 +- .../src/main/res/values/stream_video_attrs_call_app_bar.xml | 2 +- .../main/res/values/stream_video_attrs_call_container_view.xml | 2 +- .../main/res/values/stream_video_attrs_call_content_view.xml | 2 +- .../main/res/values/stream_video_attrs_call_controls_view.xml | 2 +- .../main/res/values/stream_video_attrs_call_details_view.xml | 2 +- .../res/values/stream_video_attrs_call_participant_view.xml | 2 +- .../src/main/res/values/stream_video_attrs_control_button.xml | 2 +- .../main/res/values/stream_video_attrs_incoming_call_view.xml | 2 +- .../main/res/values/stream_video_attrs_outgoing_call_view.xml | 2 +- .../res/values/stream_video_attrs_picture_in_picture_view.xml | 2 +- .../src/main/res/values/stream_video_attrs_screen_share.xml | 2 +- .../src/main/res/values/stream_video_colors.xml | 2 +- stream-video-android-ui-xml/src/main/res/values/styles.xml | 2 +- tutorials/tutorial-audio/.idea/gradle.xml | 2 +- tutorials/tutorial-audio/.idea/misc.xml | 2 +- tutorials/tutorial-audio/build.gradle.kts | 2 +- tutorials/tutorial-audio/src/main/AndroidManifest.xml | 2 +- .../io/getstream/video/android/tutorial/audio/MainActivity.kt | 2 +- .../src/main/res/drawable/ic_launcher_background.xml | 2 +- .../src/main/res/drawable/ic_launcher_foreground.xml | 2 +- .../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- .../src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml | 2 +- tutorials/tutorial-audio/src/main/res/values/colors.xml | 2 +- tutorials/tutorial-audio/src/main/res/values/strings.xml | 2 +- tutorials/tutorial-audio/src/main/res/values/themes.xml | 2 +- tutorials/tutorial-audio/src/main/res/xml/backup_rules.xml | 2 +- .../tutorial-audio/src/main/res/xml/data_extraction_rules.xml | 2 +- tutorials/tutorial-livestream/.idea/gradle.xml | 2 +- tutorials/tutorial-livestream/.idea/misc.xml | 2 +- tutorials/tutorial-livestream/.idea/workspace.xml | 2 +- tutorials/tutorial-livestream/build.gradle.kts | 2 +- tutorials/tutorial-livestream/src/main/AndroidManifest.xml | 2 +- .../io/getstream/video/android/tutorial/livestream/LiveGuest.kt | 2 +- .../io/getstream/video/android/tutorial/livestream/LiveHost.kt | 2 +- .../io/getstream/video/android/tutorial/livestream/LiveMain.kt | 2 +- .../getstream/video/android/tutorial/livestream/LiveNavHost.kt | 2 +- .../getstream/video/android/tutorial/livestream/MainActivity.kt | 2 +- .../video/android/tutorial/livestream/ui/LiveButton.kt | 2 +- .../getstream/video/android/tutorial/livestream/ui/LiveLabel.kt | 2 +- .../getstream/video/android/tutorial/livestream/ui/TimeLabel.kt | 2 +- .../src/main/res/drawable/ic_launcher_background.xml | 2 +- .../src/main/res/drawable/ic_launcher_foreground.xml | 2 +- .../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- .../src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml | 2 +- tutorials/tutorial-livestream/src/main/res/values/colors.xml | 2 +- tutorials/tutorial-livestream/src/main/res/values/strings.xml | 2 +- tutorials/tutorial-livestream/src/main/res/values/themes.xml | 2 +- tutorials/tutorial-livestream/src/main/res/xml/backup_rules.xml | 2 +- .../src/main/res/xml/data_extraction_rules.xml | 2 +- tutorials/tutorial-video/build.gradle.kts | 2 +- tutorials/tutorial-video/src/main/AndroidManifest.xml | 2 +- .../io/getstream/video/android/tutorial/video/MainActivity.kt | 2 +- .../io/getstream/video/android/tutorial/video/MainActivity2.kt | 2 +- .../io/getstream/video/android/tutorial/video/MainActivity3.kt | 2 +- .../src/main/res/drawable/ic_launcher_background.xml | 2 +- .../src/main/res/drawable/ic_launcher_foreground.xml | 2 +- .../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- .../src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml | 2 +- tutorials/tutorial-video/src/main/res/values/colors.xml | 2 +- tutorials/tutorial-video/src/main/res/values/strings.xml | 2 +- tutorials/tutorial-video/src/main/res/values/themes.xml | 2 +- tutorials/tutorial-video/src/main/res/xml/backup_rules.xml | 2 +- .../tutorial-video/src/main/res/xml/data_extraction_rules.xml | 2 +- 764 files changed, 764 insertions(+), 764 deletions(-) diff --git a/README.md b/README.md index adb05d5567..e26a6c6909 100644 --- a/README.md +++ b/README.md @@ -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 @@ + + + + + + diff --git a/demo-app/src/main/res/drawable/ic_layout_grid.xml b/demo-app/src/main/res/drawable/ic_layout_grid.xml new file mode 100644 index 0000000000..1df792b1d5 --- /dev/null +++ b/demo-app/src/main/res/drawable/ic_layout_grid.xml @@ -0,0 +1,40 @@ + + + + + + + + + + diff --git a/demo-app/src/main/res/drawable/ic_layout_spotlight.xml b/demo-app/src/main/res/drawable/ic_layout_spotlight.xml new file mode 100644 index 0000000000..9709c02dbe --- /dev/null +++ b/demo-app/src/main/res/drawable/ic_layout_spotlight.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/demo-app/src/main/res/drawable/stream_calls_logo.png b/demo-app/src/main/res/drawable/stream_calls_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0021a94cd537e2b387259e1a754ea557e6a52702 GIT binary patch literal 29075 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 VKJyD0f6Y(?lao@GtPnT;`#-dor=b7< literal 0 HcmV?d00001 diff --git a/demo-app/src/main/res/values/strings.xml b/demo-app/src/main/res/values/strings.xml index e76426d197..0102424944 100644 --- a/demo-app/src/main/res/values/strings.xml +++ b/demo-app/src/main/res/values/strings.xml @@ -27,7 +27,7 @@ Having problems logging in?  Contact us Sign out - Build reliable video calling, audio rooms, and live streaming with our easy-to-use SDKs and global edge network + Start a new call, join a meeting by \nentering the call ID or by scanning \na QR code. Join Call Scan QR meeting code You are about to join a call. %d more people are in the call. @@ -46,6 +46,7 @@ Are you sure you want to sign out? Cancel To continue into the call\nScan the QR Code + Scan QR Code Restart App Error Log Log messages copied to clipboard! diff --git a/demo-app/src/main/res/values/themes.xml b/demo-app/src/main/res/values/themes.xml index 4f45551f6b..5df71706bf 100644 --- a/demo-app/src/main/res/values/themes.xml +++ b/demo-app/src/main/res/values/themes.xml @@ -17,6 +17,6 @@ \ No newline at end of file diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index 9705e6cd73..04e2352a83 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -1117,9 +1117,13 @@ public final class io/getstream/video/android/core/call/state/SelectAudioDevice } public final class io/getstream/video/android/core/call/state/Settings : io/getstream/video/android/core/call/state/CallAction { - public static final field INSTANCE Lio/getstream/video/android/core/call/state/Settings; + public fun (Z)V + public final fun component1 ()Z + public final fun copy (Z)Lio/getstream/video/android/core/call/state/Settings; + public static synthetic fun copy$default (Lio/getstream/video/android/core/call/state/Settings;ZILjava/lang/Object;)Lio/getstream/video/android/core/call/state/Settings; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I + public final fun isEnabled ()Z public fun toString ()Ljava/lang/String; } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/state/CallAction.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/state/CallAction.kt index d030b04590..54c5b2a8a2 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/state/CallAction.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/state/CallAction.kt @@ -85,7 +85,9 @@ public data object ChatDialog : CallAction /** * Action to show a settings. */ -public data object Settings : CallAction +public data class Settings( + val isEnabled: Boolean, +) : CallAction /** * Action to show a reaction popup. diff --git a/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api b/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api index b1b2e9f425..c1adb91dc7 100644 --- a/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api +++ b/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api @@ -431,6 +431,287 @@ public final class io/getstream/video/android/compose/theme/VideoThemeKt { public static final fun VideoTheme (ZLio/getstream/video/android/compose/theme/StreamColors;Lio/getstream/video/android/compose/theme/StreamDimens;Lio/getstream/video/android/compose/theme/StreamTypography;Lio/getstream/video/android/compose/theme/StreamShapes;Landroidx/compose/material/ripple/RippleTheme;Lio/getstream/video/android/core/mapper/ReactionMapper;ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V } +public final class io/getstream/video/android/compose/theme/base/StreamColors { + public static final field $stable I + public static final field Companion Lio/getstream/video/android/compose/theme/base/StreamColors$Companion; + public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-0d7_KjU ()J + public final fun component10-0d7_KjU ()J + public final fun component11-0d7_KjU ()J + public final fun component12-0d7_KjU ()J + public final fun component13-0d7_KjU ()J + public final fun component14-0d7_KjU ()J + public final fun component15-0d7_KjU ()J + public final fun component16-0d7_KjU ()J + public final fun component17-0d7_KjU ()J + public final fun component18-0d7_KjU ()J + public final fun component19-0d7_KjU ()J + public final fun component2-0d7_KjU ()J + public final fun component20-0d7_KjU ()J + public final fun component21-0d7_KjU ()J + public final fun component22-0d7_KjU ()J + public final fun component23-0d7_KjU ()J + public final fun component24-0d7_KjU ()J + public final fun component25-0d7_KjU ()J + public final fun component26-0d7_KjU ()J + public final fun component27-0d7_KjU ()J + public final fun component28-0d7_KjU ()J + public final fun component29-0d7_KjU ()J + public final fun component3-0d7_KjU ()J + public final fun component30-0d7_KjU ()J + public final fun component31-0d7_KjU ()J + public final fun component32-0d7_KjU ()J + public final fun component33-0d7_KjU ()J + public final fun component34-0d7_KjU ()J + public final fun component35-0d7_KjU ()J + public final fun component36-0d7_KjU ()J + public final fun component37-0d7_KjU ()J + public final fun component38-0d7_KjU ()J + public final fun component39-0d7_KjU ()J + public final fun component4-0d7_KjU ()J + public final fun component40-0d7_KjU ()J + public final fun component5-0d7_KjU ()J + public final fun component6-0d7_KjU ()J + public final fun component7-0d7_KjU ()J + public final fun component8-0d7_KjU ()J + public final fun component9-0d7_KjU ()J + public final fun copy-KR53e2g (JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ)Lio/getstream/video/android/compose/theme/base/StreamColors; + public static synthetic fun copy-KR53e2g$default (Lio/getstream/video/android/compose/theme/base/StreamColors;JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIILjava/lang/Object;)Lio/getstream/video/android/compose/theme/base/StreamColors; + public fun equals (Ljava/lang/Object;)Z + public final fun getAlertCaution-0d7_KjU ()J + public final fun getAlertSuccess-0d7_KjU ()J + public final fun getAlertWarning-0d7_KjU ()J + public final fun getBasePrimary-0d7_KjU ()J + public final fun getBaseQuaternary-0d7_KjU ()J + public final fun getBaseQuinary-0d7_KjU ()J + public final fun getBaseSecondary-0d7_KjU ()J + public final fun getBaseSenary-0d7_KjU ()J + public final fun getBaseSheetPrimary-0d7_KjU ()J + public final fun getBaseSheetQuarternary-0d7_KjU ()J + public final fun getBaseSheetSecondary-0d7_KjU ()J + public final fun getBaseSheetTertiary-0d7_KjU ()J + public final fun getBaseTertiary-0d7_KjU ()J + public final fun getBrandCyan-0d7_KjU ()J + public final fun getBrandGreen-0d7_KjU ()J + public final fun getBrandMaroon-0d7_KjU ()J + public final fun getBrandPrimary-0d7_KjU ()J + public final fun getBrandPrimaryDk-0d7_KjU ()J + public final fun getBrandPrimaryLt-0d7_KjU ()J + public final fun getBrandRed-0d7_KjU ()J + public final fun getBrandRedDk-0d7_KjU ()J + public final fun getBrandRedLt-0d7_KjU ()J + public final fun getBrandSecondary-0d7_KjU ()J + public final fun getBrandSecondaryTransparent-0d7_KjU ()J + public final fun getBrandViolet-0d7_KjU ()J + public final fun getBrandYellow-0d7_KjU ()J + public final fun getButtonAlertDefault-0d7_KjU ()J + public final fun getButtonAlertDisabled-0d7_KjU ()J + public final fun getButtonAlertPressed-0d7_KjU ()J + public final fun getButtonBrandDefault-0d7_KjU ()J + public final fun getButtonBrandDisabled-0d7_KjU ()J + public final fun getButtonBrandPressed-0d7_KjU ()J + public final fun getButtonPrimaryDefault-0d7_KjU ()J + public final fun getButtonPrimaryDisabled-0d7_KjU ()J + public final fun getButtonPrimaryPressed-0d7_KjU ()J + public final fun getIconActive-0d7_KjU ()J + public final fun getIconAlert-0d7_KjU ()J + public final fun getIconDefault-0d7_KjU ()J + public final fun getIconDisabled-0d7_KjU ()J + public final fun getIconPressed-0d7_KjU ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/video/android/compose/theme/base/StreamColors$Companion { + public final fun defaultColors (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/theme/base/StreamColors; +} + +public final class io/getstream/video/android/compose/theme/base/StreamDimens { + public static final field $stable I + public static final field Companion Lio/getstream/video/android/compose/theme/base/StreamDimens$Companion; + public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-D9Ej5fM ()F + public final fun component10-D9Ej5fM ()F + public final fun component11-D9Ej5fM ()F + public final fun component12-D9Ej5fM ()F + public final fun component13-D9Ej5fM ()F + public final fun component14-D9Ej5fM ()F + public final fun component15-D9Ej5fM ()F + public final fun component16-D9Ej5fM ()F + public final fun component17-D9Ej5fM ()F + public final fun component18-D9Ej5fM ()F + public final fun component19-D9Ej5fM ()F + public final fun component2-D9Ej5fM ()F + public final fun component20-D9Ej5fM ()F + public final fun component21-D9Ej5fM ()F + public final fun component22-D9Ej5fM ()F + public final fun component23-D9Ej5fM ()F + public final fun component24-D9Ej5fM ()F + public final fun component25-D9Ej5fM ()F + public final fun component26-D9Ej5fM ()F + public final fun component27-D9Ej5fM ()F + public final fun component28-XSAIIZE ()J + public final fun component29-XSAIIZE ()J + public final fun component3-D9Ej5fM ()F + public final fun component30-XSAIIZE ()J + public final fun component31-XSAIIZE ()J + public final fun component32-XSAIIZE ()J + public final fun component33-XSAIIZE ()J + public final fun component34-XSAIIZE ()J + public final fun component35-XSAIIZE ()J + public final fun component36-XSAIIZE ()J + public final fun component37-XSAIIZE ()J + public final fun component38-XSAIIZE ()J + public final fun component39-XSAIIZE ()J + public final fun component4-D9Ej5fM ()F + public final fun component5-D9Ej5fM ()F + public final fun component6-D9Ej5fM ()F + public final fun component7-D9Ej5fM ()F + public final fun component8-D9Ej5fM ()F + public final fun component9-D9Ej5fM ()F + public final fun copy-unn3US4 (FFFFFFFFFFFFFFFFFFFFFFFFFFFJJJJJJJJJJJJ)Lio/getstream/video/android/compose/theme/base/StreamDimens; + public static synthetic fun copy-unn3US4$default (Lio/getstream/video/android/compose/theme/base/StreamDimens;FFFFFFFFFFFFFFFFFFFFFFFFFFFJJJJJJJJJJJJIILjava/lang/Object;)Lio/getstream/video/android/compose/theme/base/StreamDimens; + public fun equals (Ljava/lang/Object;)Z + public final fun getComponentHeightL-D9Ej5fM ()F + public final fun getComponentHeightM-D9Ej5fM ()F + public final fun getComponentHeightS-D9Ej5fM ()F + public final fun getComponentPaddingBottom-D9Ej5fM ()F + public final fun getComponentPaddingEnd-D9Ej5fM ()F + public final fun getComponentPaddingFixed-D9Ej5fM ()F + public final fun getComponentPaddingStart-D9Ej5fM ()F + public final fun getComponentPaddingTop-D9Ej5fM ()F + public final fun getGeneric3xl-D9Ej5fM ()F + public final fun getGenericL-D9Ej5fM ()F + public final fun getGenericM-D9Ej5fM ()F + public final fun getGenericMax-D9Ej5fM ()F + public final fun getGenericS-D9Ej5fM ()F + public final fun getGenericXXs-D9Ej5fM ()F + public final fun getGenericXl-D9Ej5fM ()F + public final fun getGenericXs-D9Ej5fM ()F + public final fun getGenericXxl-D9Ej5fM ()F + public final fun getLineHeightL-XSAIIZE ()J + public final fun getLineHeightM-XSAIIZE ()J + public final fun getLineHeightS-XSAIIZE ()J + public final fun getLineHeightXl-XSAIIZE ()J + public final fun getLineHeightXs-XSAIIZE ()J + public final fun getLineHeightXxl-XSAIIZE ()J + public final fun getRoundnessL-D9Ej5fM ()F + public final fun getRoundnessM-D9Ej5fM ()F + public final fun getRoundnessS-D9Ej5fM ()F + public final fun getRoundnessXl-D9Ej5fM ()F + public final fun getSpacingL-D9Ej5fM ()F + public final fun getSpacingM-D9Ej5fM ()F + public final fun getSpacingS-D9Ej5fM ()F + public final fun getSpacingXXs-D9Ej5fM ()F + public final fun getSpacingXl-D9Ej5fM ()F + public final fun getSpacingXs-D9Ej5fM ()F + public final fun getTextSizeL-XSAIIZE ()J + public final fun getTextSizeM-XSAIIZE ()J + public final fun getTextSizeS-XSAIIZE ()J + public final fun getTextSizeXl-XSAIIZE ()J + public final fun getTextSizeXs-XSAIIZE ()J + public final fun getTextSizeXxl-XSAIIZE ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/video/android/compose/theme/base/StreamDimens$Companion { + public final fun defaultDimens (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/theme/base/StreamDimens; +} + +public final class io/getstream/video/android/compose/theme/base/StreamShapes { + public static final field $stable I + public static final field Companion Lio/getstream/video/android/compose/theme/base/StreamShapes$Companion; + public fun (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)V + public final fun component1 ()Landroidx/compose/ui/graphics/Shape; + public final fun component2 ()Landroidx/compose/ui/graphics/Shape; + public final fun component3 ()Landroidx/compose/ui/graphics/Shape; + public final fun component4 ()Landroidx/compose/ui/graphics/Shape; + public final fun component5 ()Landroidx/compose/ui/graphics/Shape; + public final fun component6 ()Landroidx/compose/ui/graphics/Shape; + public final fun component7 ()Landroidx/compose/ui/graphics/Shape; + public final fun component8 ()Landroidx/compose/ui/graphics/Shape; + public final fun copy (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)Lio/getstream/video/android/compose/theme/base/StreamShapes; + public static synthetic fun copy$default (Lio/getstream/video/android/compose/theme/base/StreamShapes;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;ILjava/lang/Object;)Lio/getstream/video/android/compose/theme/base/StreamShapes; + public fun equals (Ljava/lang/Object;)Z + public final fun getButton ()Landroidx/compose/ui/graphics/Shape; + public final fun getCircle ()Landroidx/compose/ui/graphics/Shape; + public final fun getContainer ()Landroidx/compose/ui/graphics/Shape; + public final fun getDialog ()Landroidx/compose/ui/graphics/Shape; + public final fun getIndicator ()Landroidx/compose/ui/graphics/Shape; + public final fun getInput ()Landroidx/compose/ui/graphics/Shape; + public final fun getSheet ()Landroidx/compose/ui/graphics/Shape; + public final fun getSquare ()Landroidx/compose/ui/graphics/Shape; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/video/android/compose/theme/base/StreamShapes$Companion { + public final fun defaultShapes (Lio/getstream/video/android/compose/theme/base/StreamDimens;Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/theme/base/StreamShapes; +} + +public abstract interface class io/getstream/video/android/compose/theme/base/StreamTheme { + public fun getColors (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/theme/base/StreamColors; + public fun getDimens (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/theme/base/StreamDimens; + public fun getReactionMapper (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/core/mapper/ReactionMapper; + public fun getRippleTheme (Landroidx/compose/runtime/Composer;I)Landroidx/compose/material/ripple/RippleTheme; + public fun getShapes (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/theme/base/StreamShapes; + public fun getStyles (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/ui/components/base/styling/CompositeStyleProvider; + public fun getTypography (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/theme/base/StreamTypography; +} + +public final class io/getstream/video/android/compose/theme/base/StreamTypography { + public static final field $stable I + public static final field Companion Lio/getstream/video/android/compose/theme/base/StreamTypography$Companion; + public fun (Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;)V + public final fun component1 ()Landroidx/compose/ui/text/TextStyle; + public final fun component10 ()Landroidx/compose/ui/text/TextStyle; + public final fun component11 ()Landroidx/compose/ui/text/TextStyle; + public final fun component12 ()Landroidx/compose/ui/text/TextStyle; + public final fun component13 ()Landroidx/compose/ui/text/TextStyle; + public final fun component14 ()Landroidx/compose/ui/text/TextStyle; + public final fun component2 ()Landroidx/compose/ui/text/TextStyle; + public final fun component3 ()Landroidx/compose/ui/text/TextStyle; + public final fun component4 ()Landroidx/compose/ui/text/TextStyle; + public final fun component5 ()Landroidx/compose/ui/text/TextStyle; + public final fun component6 ()Landroidx/compose/ui/text/TextStyle; + public final fun component7 ()Landroidx/compose/ui/text/TextStyle; + public final fun component8 ()Landroidx/compose/ui/text/TextStyle; + public final fun component9 ()Landroidx/compose/ui/text/TextStyle; + public final fun copy (Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;)Lio/getstream/video/android/compose/theme/base/StreamTypography; + public static synthetic fun copy$default (Lio/getstream/video/android/compose/theme/base/StreamTypography;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;ILjava/lang/Object;)Lio/getstream/video/android/compose/theme/base/StreamTypography; + public fun equals (Ljava/lang/Object;)Z + public final fun getBodyL ()Landroidx/compose/ui/text/TextStyle; + public final fun getBodyM ()Landroidx/compose/ui/text/TextStyle; + public final fun getBodyS ()Landroidx/compose/ui/text/TextStyle; + public final fun getLabelL ()Landroidx/compose/ui/text/TextStyle; + public final fun getLabelM ()Landroidx/compose/ui/text/TextStyle; + public final fun getLabelS ()Landroidx/compose/ui/text/TextStyle; + public final fun getLabelXS ()Landroidx/compose/ui/text/TextStyle; + public final fun getSubtitleL ()Landroidx/compose/ui/text/TextStyle; + public final fun getSubtitleM ()Landroidx/compose/ui/text/TextStyle; + public final fun getSubtitleS ()Landroidx/compose/ui/text/TextStyle; + public final fun getTitleL ()Landroidx/compose/ui/text/TextStyle; + public final fun getTitleM ()Landroidx/compose/ui/text/TextStyle; + public final fun getTitleS ()Landroidx/compose/ui/text/TextStyle; + public final fun getTitleXs ()Landroidx/compose/ui/text/TextStyle; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/video/android/compose/theme/base/StreamTypography$Companion { + public final fun defaultTypography (Lio/getstream/video/android/compose/theme/base/StreamColors;Lio/getstream/video/android/compose/theme/base/StreamDimens;Landroidx/compose/ui/text/font/FontFamily;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/theme/base/StreamTypography; +} + +public final class io/getstream/video/android/compose/theme/base/VideoTheme : io/getstream/video/android/compose/theme/base/StreamTheme { + public static final field $stable I + public static final field INSTANCE Lio/getstream/video/android/compose/theme/base/VideoTheme; +} + +public final class io/getstream/video/android/compose/theme/base/VideoThemeKt { + public static final fun VideoTheme (ZLio/getstream/video/android/compose/theme/base/StreamColors;Lio/getstream/video/android/compose/theme/base/StreamDimens;Lio/getstream/video/android/compose/theme/base/StreamTypography;Lio/getstream/video/android/compose/theme/base/StreamShapes;Landroidx/compose/material/ripple/RippleTheme;Lio/getstream/video/android/core/mapper/ReactionMapper;ZLio/getstream/video/android/compose/ui/components/base/styling/CompositeStyleProvider;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V +} + public final class io/getstream/video/android/compose/ui/components/audio/AudioAppBarKt { public static final fun AudioAppBar (Landroidx/compose/ui/Modifier;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)V } @@ -471,11 +752,9 @@ public final class io/getstream/video/android/compose/ui/components/audio/Compos public final class io/getstream/video/android/compose/ui/components/audio/ComposableSingletons$AudioControlActionsKt { public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/audio/ComposableSingletons$AudioControlActionsKt; - public static field lambda-1 Lkotlin/jvm/functions/Function3; - public static field lambda-2 Lkotlin/jvm/functions/Function2; + public static field lambda-1 Lkotlin/jvm/functions/Function2; public fun ()V - public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; - public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/video/android/compose/ui/components/audio/ComposableSingletons$AudioParticipantsGridKt { @@ -532,7 +811,7 @@ public final class io/getstream/video/android/compose/ui/components/audio/Regula } public final class io/getstream/video/android/compose/ui/components/avatar/AvatarKt { - public static final fun Avatar-br9S7oA (Landroidx/compose/ui/Modifier;Ljava/lang/String;Ljava/lang/String;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/layout/ContentScale;Ljava/lang/String;JILjava/lang/Integer;JLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V + public static final fun Avatar-zf1_rLo (Landroidx/compose/ui/Modifier;Ljava/lang/String;Ljava/lang/String;Landroidx/compose/ui/graphics/Shape;Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/layout/ContentScale;Ljava/lang/String;JILjava/lang/Integer;JLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/video/android/compose/ui/components/avatar/ComposableSingletons$AvatarKt { @@ -586,16 +865,15 @@ public final class io/getstream/video/android/compose/ui/components/avatar/Onlin } public final class io/getstream/video/android/compose/ui/components/avatar/UserAvatarBackgroundKt { - public static final fun UserAvatarBackground-5WCoS_E (Ljava/lang/String;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;FFLandroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/layout/ContentScale;Ljava/lang/String;JJILjava/lang/Integer;Landroidx/compose/runtime/Composer;III)V + public static final fun UserAvatarBackground-xr0SGnQ (Ljava/lang/String;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;FLandroidx/compose/ui/layout/ContentScale;Ljava/lang/String;JJILjava/lang/Integer;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/video/android/compose/ui/components/avatar/UserAvatarKt { - public static final fun UserAvatar-8W8krDU (Ljava/lang/String;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/layout/ContentScale;Ljava/lang/String;JILjava/lang/Integer;JZLio/getstream/video/android/compose/ui/components/avatar/OnlineIndicatorAlignment;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V + public static final fun UserAvatar-8W8krDU (Ljava/lang/String;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/layout/ContentScale;Ljava/lang/String;Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;JILjava/lang/Integer;JZLio/getstream/video/android/compose/ui/components/avatar/OnlineIndicatorAlignment;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/video/android/compose/ui/components/background/CallBackgroundKt { - public static final fun CallBackground (Landroidx/compose/ui/Modifier;Ljava/util/List;ZZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V - public static final fun ParticipantImageBackground (Ljava/lang/String;Landroidx/compose/ui/Modifier;ILandroidx/compose/runtime/Composer;II)V + public static final fun CallBackground (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/video/android/compose/ui/components/background/ComposableSingletons$CallBackgroundKt { @@ -607,6 +885,381 @@ public final class io/getstream/video/android/compose/ui/components/background/C public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/video/android/compose/ui/components/base/BadgeKt { + public static final fun StreamBadgeBox (Landroidx/compose/ui/Modifier;Ljava/lang/String;ZLio/getstream/video/android/compose/ui/components/base/styling/BadgeStyle;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V +} + +public final class io/getstream/video/android/compose/ui/components/base/ComposableSingletons$BadgeKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/base/ComposableSingletons$BadgeKt; + public static field lambda-1 Lkotlin/jvm/functions/Function4; + public static field lambda-2 Lkotlin/jvm/functions/Function4; + public static field lambda-3 Lkotlin/jvm/functions/Function4; + public static field lambda-4 Lkotlin/jvm/functions/Function4; + public static field lambda-5 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-3$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-4$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-5$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; +} + +public final class io/getstream/video/android/compose/ui/components/base/ComposableSingletons$ContainerKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/base/ComposableSingletons$ContainerKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; +} + +public final class io/getstream/video/android/compose/ui/components/base/ComposableSingletons$DialogsKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/base/ComposableSingletons$DialogsKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public static field lambda-2 Lkotlin/jvm/functions/Function2; + public static field lambda-3 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-3$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; +} + +public final class io/getstream/video/android/compose/ui/components/base/ComposableSingletons$InputFieldsKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/base/ComposableSingletons$InputFieldsKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; +} + +public final class io/getstream/video/android/compose/ui/components/base/ComposableSingletons$StreamButtonKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/base/ComposableSingletons$StreamButtonKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public static field lambda-2 Lkotlin/jvm/functions/Function2; + public static field lambda-3 Lkotlin/jvm/functions/Function2; + public static field lambda-4 Lkotlin/jvm/functions/Function2; + public static field lambda-5 Lkotlin/jvm/functions/Function4; + public static field lambda-6 Lkotlin/jvm/functions/Function2; + public static field lambda-7 Lkotlin/jvm/functions/Function2; + public static field lambda-8 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-3$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-4$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-5$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-6$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-7$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-8$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; +} + +public final class io/getstream/video/android/compose/ui/components/base/ContainerKt { + public static final fun GenericContainer-BazWgJc (Landroidx/compose/ui/Modifier;JFLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V +} + +public final class io/getstream/video/android/compose/ui/components/base/DialogsKt { + public static final fun StreamDialog (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/compose/ui/components/base/styling/DialogStyle;Landroidx/compose/ui/window/DialogProperties;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V + public static final fun StreamDialogPositiveNegative (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/DialogProperties;Lio/getstream/video/android/compose/ui/components/base/styling/DialogStyle;Ljava/lang/String;Landroidx/compose/ui/graphics/vector/ImageVector;Ljava/lang/String;Lkotlin/Triple;Lkotlin/Triple;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V +} + +public final class io/getstream/video/android/compose/ui/components/base/InputFieldsKt { + public static final fun StreamOutlinedTextField (Landroidx/compose/ui/Modifier;Landroidx/compose/ui/text/input/TextFieldValue;Lkotlin/jvm/functions/Function1;ZZLio/getstream/video/android/compose/ui/components/base/styling/TextFieldStyle;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/ui/text/input/VisualTransformation;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIILkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/runtime/Composer;III)V + public static final fun StreamTextField (Landroidx/compose/ui/Modifier;Landroidx/compose/ui/text/input/TextFieldValue;Lkotlin/jvm/functions/Function1;ZZLio/getstream/video/android/compose/ui/components/base/styling/TextFieldStyle;Ljava/lang/String;ZIILandroidx/compose/ui/graphics/vector/ImageVector;Landroidx/compose/ui/text/input/VisualTransformation;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/runtime/Composer;III)V +} + +public final class io/getstream/video/android/compose/ui/components/base/StreamButtonKt { + public static final fun GenericStreamButton (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;ZLio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle;Landroidx/compose/foundation/interaction/MutableInteractionSource;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V + public static final fun GenericToggleButton (Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/State;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V + public static final fun StreamButton (Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/vector/ImageVector;Ljava/lang/String;ZZLio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle;Landroidx/compose/foundation/interaction/MutableInteractionSource;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V + public static final fun StreamIconButton (Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/vector/ImageVector;Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle;Lkotlin/jvm/functions/Function0;ZZLandroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/runtime/Composer;II)V + public static final fun StreamIconToggleButton (Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/State;Landroidx/compose/ui/graphics/vector/ImageVector;Landroidx/compose/ui/graphics/vector/ImageVector;Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle;ZLio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle;ZLandroidx/compose/foundation/interaction/MutableInteractionSource;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun StreamToggleButton (Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/State;Ljava/lang/String;Ljava/lang/String;Landroidx/compose/ui/graphics/vector/ImageVector;Landroidx/compose/ui/graphics/vector/ImageVector;Lio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle;ZLandroidx/compose/foundation/interaction/MutableInteractionSource;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/BadgeStyle : io/getstream/video/android/compose/ui/components/base/styling/StreamStyle { + public static final field $stable I + public synthetic fun (FJLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-D9Ej5fM ()F + public final fun component2-0d7_KjU ()J + public final fun component3 ()Landroidx/compose/ui/text/TextStyle; + public final fun component4 ()Landroidx/compose/foundation/layout/PaddingValues; + public final fun copy-1jbw_BE (FJLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/layout/PaddingValues;)Lio/getstream/video/android/compose/ui/components/base/styling/BadgeStyle; + public static synthetic fun copy-1jbw_BE$default (Lio/getstream/video/android/compose/ui/components/base/styling/BadgeStyle;FJLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/layout/PaddingValues;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/base/styling/BadgeStyle; + public fun equals (Ljava/lang/Object;)Z + public final fun getColor-0d7_KjU ()J + public final fun getContentPaddings ()Landroidx/compose/foundation/layout/PaddingValues; + public final fun getSize-D9Ej5fM ()F + public final fun getTextStyle ()Landroidx/compose/ui/text/TextStyle; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class io/getstream/video/android/compose/ui/components/base/styling/BadgeStyleProvider { + public static final field $stable I + public fun ()V + public final fun defaultBadgeStyle (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/ui/components/base/styling/BadgeStyle; +} + +public class io/getstream/video/android/compose/ui/components/base/styling/ButtonStyleProvider { + public static final field $stable I + public fun ()V + public final fun alertButtonStyle (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle; + public final fun alertIconButtonStyle (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle; + public final fun genericButtonStyle (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Landroidx/compose/material/ButtonElevation;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/foundation/BorderStroke;Landroidx/compose/material/ButtonColors;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle; + public final fun onlyIconIconButtonStyle (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle; + public final fun primaryButtonStyle (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle; + public final fun primaryIconButtonStyle (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle; + public final fun secondaryButtonStyle (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle; + public final fun secondaryIconButtonStyle (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle; + public final fun tetriaryButtonStyle (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle; + public final fun tetriaryIconButtonStyle (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle; + public final fun toggleButtonStyleOff (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle; + public final fun toggleButtonStyleOn (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle; +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/ButtonStyles : io/getstream/video/android/compose/ui/components/base/styling/ButtonStyleProvider { + public static final field $stable I + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/base/styling/ButtonStyles; +} + +public class io/getstream/video/android/compose/ui/components/base/styling/CompositeStyleProvider { + public static final field $stable I + public fun ()V + public fun (Lio/getstream/video/android/compose/ui/components/base/styling/IconStyleProvider;Lio/getstream/video/android/compose/ui/components/base/styling/TextFieldStyleProvider;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleProvider;Lio/getstream/video/android/compose/ui/components/base/styling/ButtonStyleProvider;Lio/getstream/video/android/compose/ui/components/base/styling/DialogStyleProvider;Lio/getstream/video/android/compose/ui/components/base/styling/BadgeStyleProvider;)V + public synthetic fun (Lio/getstream/video/android/compose/ui/components/base/styling/IconStyleProvider;Lio/getstream/video/android/compose/ui/components/base/styling/TextFieldStyleProvider;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleProvider;Lio/getstream/video/android/compose/ui/components/base/styling/ButtonStyleProvider;Lio/getstream/video/android/compose/ui/components/base/styling/DialogStyleProvider;Lio/getstream/video/android/compose/ui/components/base/styling/BadgeStyleProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getBadgeStyles ()Lio/getstream/video/android/compose/ui/components/base/styling/BadgeStyleProvider; + public final fun getButtonStyles ()Lio/getstream/video/android/compose/ui/components/base/styling/ButtonStyleProvider; + public final fun getDialogStyles ()Lio/getstream/video/android/compose/ui/components/base/styling/DialogStyleProvider; + public final fun getIconStyles ()Lio/getstream/video/android/compose/ui/components/base/styling/IconStyleProvider; + public final fun getTextFieldStyles ()Lio/getstream/video/android/compose/ui/components/base/styling/TextFieldStyleProvider; + public final fun getTextStyles ()Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleProvider; +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/DialogStyle : io/getstream/video/android/compose/ui/components/base/styling/StreamStyle { + public static final field $stable I + public synthetic fun (Landroidx/compose/ui/graphics/Shape;JLandroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle;Landroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Landroidx/compose/ui/graphics/Shape; + public final fun component2-0d7_KjU ()J + public final fun component3 ()Landroidx/compose/ui/text/TextStyle; + public final fun component4 ()Landroidx/compose/ui/text/TextStyle; + public final fun component5 ()Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle; + public final fun component6 ()Landroidx/compose/foundation/layout/PaddingValues; + public final fun copy-3IgeMak (Landroidx/compose/ui/graphics/Shape;JLandroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle;Landroidx/compose/foundation/layout/PaddingValues;)Lio/getstream/video/android/compose/ui/components/base/styling/DialogStyle; + public static synthetic fun copy-3IgeMak$default (Lio/getstream/video/android/compose/ui/components/base/styling/DialogStyle;Landroidx/compose/ui/graphics/Shape;JLandroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle;Landroidx/compose/foundation/layout/PaddingValues;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/base/styling/DialogStyle; + public fun equals (Ljava/lang/Object;)Z + public final fun getBackgroundColor-0d7_KjU ()J + public final fun getContentPaddings ()Landroidx/compose/foundation/layout/PaddingValues; + public final fun getContentTextStyle ()Landroidx/compose/ui/text/TextStyle; + public final fun getIconStyle ()Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle; + public final fun getShape ()Landroidx/compose/ui/graphics/Shape; + public final fun getTitleStyle ()Landroidx/compose/ui/text/TextStyle; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class io/getstream/video/android/compose/ui/components/base/styling/DialogStyleProvider { + public static final field $stable I + public fun ()V + public final fun defaultDialogStyle (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/ui/components/base/styling/DialogStyle; +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/IconStyle : io/getstream/video/android/compose/ui/components/base/styling/StreamStyle { + public static final field $stable I + public synthetic fun (JLandroidx/compose/foundation/layout/PaddingValues;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JLandroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-0d7_KjU ()J + public final fun component2 ()Landroidx/compose/foundation/layout/PaddingValues; + public final fun copy-DxMtmZc (JLandroidx/compose/foundation/layout/PaddingValues;)Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle; + public static synthetic fun copy-DxMtmZc$default (Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle;JLandroidx/compose/foundation/layout/PaddingValues;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle; + public fun equals (Ljava/lang/Object;)Z + public final fun getColor-0d7_KjU ()J + public final fun getPadding ()Landroidx/compose/foundation/layout/PaddingValues; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class io/getstream/video/android/compose/ui/components/base/styling/IconStyleProvider { + public static final field $stable I + public fun ()V + public final fun customColorIconStyle-ek8zF_U (JLandroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle; + public final fun defaultIconStyle-GyCwops (Landroidx/compose/foundation/layout/PaddingValues;JJJLandroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle; +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/IconStyles : io/getstream/video/android/compose/ui/components/base/styling/IconStyleProvider { + public static final field $stable I + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/base/styling/IconStyles; +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/StreamBadgeStyles : io/getstream/video/android/compose/ui/components/base/styling/BadgeStyleProvider { + public static final field $stable I + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/base/styling/StreamBadgeStyles; +} + +public class io/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle { + public static final field $stable I + public fun (Landroidx/compose/material/ButtonElevation;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/foundation/BorderStroke;Landroidx/compose/material/ButtonColors;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle;)V + public final fun copy (Landroidx/compose/material/ButtonElevation;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/foundation/BorderStroke;Landroidx/compose/material/ButtonColors;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle;)Lio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle; + public static synthetic fun copy$default (Lio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle;Landroidx/compose/material/ButtonElevation;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/foundation/BorderStroke;Landroidx/compose/material/ButtonColors;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle; + public final fun getBorder ()Landroidx/compose/foundation/BorderStroke; + public final fun getColors ()Landroidx/compose/material/ButtonColors; + public final fun getContentPadding ()Landroidx/compose/foundation/layout/PaddingValues; + public final fun getElevation ()Landroidx/compose/material/ButtonElevation; + public final fun getIconStyle ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle; + public final fun getShape ()Landroidx/compose/ui/graphics/Shape; + public final fun getTextStyle ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/StreamDialogStyles : io/getstream/video/android/compose/ui/components/base/styling/DialogStyleProvider { + public static final field $stable I + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/base/styling/StreamDialogStyles; +} + +public class io/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle : io/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle { + public static final field $stable I + public static final field Companion Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle$Companion; + public synthetic fun (FFLandroidx/compose/material/ButtonElevation;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/foundation/BorderStroke;Landroidx/compose/material/ButtonColors;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun copyFixed-X35cekY (FFLandroidx/compose/material/ButtonElevation;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/foundation/BorderStroke;Landroidx/compose/material/ButtonColors;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle;)Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle; + public static synthetic fun copyFixed-X35cekY$default (Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle;FFLandroidx/compose/material/ButtonElevation;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/foundation/BorderStroke;Landroidx/compose/material/ButtonColors;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle; + public final fun getHeight-D9Ej5fM ()F + public final fun getWidth-D9Ej5fM ()F +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle$Companion { + public final fun of-Md-fbLM (FFLio/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle;)Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle; +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle : io/getstream/video/android/compose/ui/components/base/styling/StreamStateStyle { + public static final field $stable I + public fun (Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle;Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle;Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle;)V + public final fun component1 ()Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle; + public final fun component2 ()Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle; + public final fun component3 ()Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle; + public final fun copy (Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle;Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle;Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle;)Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle; + public static synthetic fun copy$default (Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle;Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle;Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle;Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle; + public fun equals (Ljava/lang/Object;)Z + public fun getDefault ()Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle; + public synthetic fun getDefault ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamStyle; + public fun getDisabled ()Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle; + public synthetic fun getDisabled ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamStyle; + public fun getPressed ()Lio/getstream/video/android/compose/ui/components/base/styling/IconStyle; + public synthetic fun getPressed ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamStyle; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class io/getstream/video/android/compose/ui/components/base/styling/StreamStateStyle { + public abstract fun getDefault ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamStyle; + public abstract fun getDisabled ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamStyle; + public abstract fun getPressed ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamStyle; + public fun of (Lio/getstream/video/android/compose/ui/components/base/styling/StyleState;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State; +} + +public abstract interface class io/getstream/video/android/compose/ui/components/base/styling/StreamStyle { +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/StreamTextFieldStyles : io/getstream/video/android/compose/ui/components/base/styling/TextFieldStyleProvider { + public static final field $stable I + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextFieldStyles; +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle : io/getstream/video/android/compose/ui/components/base/styling/StreamStateStyle { + public static final field $stable I + public fun (Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;)V + public final fun component1 ()Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper; + public final fun component2 ()Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper; + public final fun component3 ()Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper; + public final fun copy (Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;)Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; + public static synthetic fun copy$default (Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; + public fun equals (Ljava/lang/Object;)Z + public synthetic fun getDefault ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamStyle; + public fun getDefault ()Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper; + public synthetic fun getDisabled ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamStyle; + public fun getDisabled ()Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper; + public synthetic fun getPressed ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamStyle; + public fun getPressed ()Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/StreamTextStyles : io/getstream/video/android/compose/ui/components/base/styling/TextStyleProvider { + public static final field $stable I + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyles; +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/StyleSize : java/lang/Enum { + public static final field L Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize; + public static final field M Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize; + public static final field S Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize; + public static final field XL Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize; + public static final field XS Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize; + public static final field XXL Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize; + public static fun values ()[Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize; +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/StyleState : java/lang/Enum { + public static final field DISABLED Lio/getstream/video/android/compose/ui/components/base/styling/StyleState; + public static final field ENABLED Lio/getstream/video/android/compose/ui/components/base/styling/StyleState; + public static final field PRESSED Lio/getstream/video/android/compose/ui/components/base/styling/StyleState; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/getstream/video/android/compose/ui/components/base/styling/StyleState; + public static fun values ()[Lio/getstream/video/android/compose/ui/components/base/styling/StyleState; +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/TextFieldStyle : io/getstream/video/android/compose/ui/components/base/styling/StreamStyle { + public static final field $stable I + public fun (Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle;Landroidx/compose/material/TextFieldColors;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/foundation/BorderStroke;Landroidx/compose/foundation/layout/PaddingValues;)V + public final fun component1 ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; + public final fun component2 ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; + public final fun component3 ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle; + public final fun component4 ()Landroidx/compose/material/TextFieldColors; + public final fun component5 ()Landroidx/compose/ui/graphics/Shape; + public final fun component6 ()Landroidx/compose/foundation/BorderStroke; + public final fun component7 ()Landroidx/compose/foundation/layout/PaddingValues; + public final fun copy (Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle;Landroidx/compose/material/TextFieldColors;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/foundation/BorderStroke;Landroidx/compose/foundation/layout/PaddingValues;)Lio/getstream/video/android/compose/ui/components/base/styling/TextFieldStyle; + public static synthetic fun copy$default (Lio/getstream/video/android/compose/ui/components/base/styling/TextFieldStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle;Landroidx/compose/material/TextFieldColors;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/foundation/BorderStroke;Landroidx/compose/foundation/layout/PaddingValues;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/base/styling/TextFieldStyle; + public fun equals (Ljava/lang/Object;)Z + public final fun getBorderStroke ()Landroidx/compose/foundation/BorderStroke; + public final fun getColors ()Landroidx/compose/material/TextFieldColors; + public final fun getIconStyle ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle; + public final fun getPaddings ()Landroidx/compose/foundation/layout/PaddingValues; + public final fun getPlaceholderStyle ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; + public final fun getShape ()Landroidx/compose/ui/graphics/Shape; + public final fun getTextStyle ()Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class io/getstream/video/android/compose/ui/components/base/styling/TextFieldStyleProvider { + public static final field $stable I + public fun ()V + public final fun defaultTextField (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamIconStyle;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/TextFieldStyle; +} + +public class io/getstream/video/android/compose/ui/components/base/styling/TextStyleProvider { + public static final field $stable I + public fun ()V + public final fun defaultBadgeTextStyle (Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; + public final fun defaultBody (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; + public final fun defaultButtonLabel (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; + public final fun defaultLabel (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; + public final fun defaultSubtitle (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; + public final fun defaultTextField (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; + public final fun defaultTitle (Lio/getstream/video/android/compose/ui/components/base/styling/StyleSize;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Landroidx/compose/runtime/Composer;II)Lio/getstream/video/android/compose/ui/components/base/styling/StreamTextStyle; +} + +public final class io/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper : io/getstream/video/android/compose/ui/components/base/styling/StreamStyle { + public static final field $stable I + public fun (Landroidx/compose/ui/text/TextStyle;)V + public final fun component1 ()Landroidx/compose/ui/text/TextStyle; + public final fun copy (Landroidx/compose/ui/text/TextStyle;)Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper; + public static synthetic fun copy$default (Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper;Landroidx/compose/ui/text/TextStyle;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/base/styling/TextStyleWrapper; + public fun equals (Ljava/lang/Object;)Z + public final fun getPlatform ()Landroidx/compose/ui/text/TextStyle; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/video/android/compose/ui/components/call/CallAppBarKt { public static final fun CallAppBar (Lio/getstream/video/android/core/Call;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V } @@ -614,10 +1267,20 @@ public final class io/getstream/video/android/compose/ui/components/call/CallApp public final class io/getstream/video/android/compose/ui/components/call/ComposableSingletons$CallAppBarKt { public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/ComposableSingletons$CallAppBarKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; - public static field lambda-2 Lkotlin/jvm/functions/Function2; + public static field lambda-2 Lkotlin/jvm/functions/Function3; + public static field lambda-3 Lkotlin/jvm/functions/Function3; + public static field lambda-4 Lkotlin/jvm/functions/Function3; + public static field lambda-5 Lkotlin/jvm/functions/Function3; + public static field lambda-6 Lkotlin/jvm/functions/Function3; + public static field lambda-7 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-3$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-4$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-5$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-6$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-7$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/video/android/compose/ui/components/call/activecall/CallContentKt { @@ -631,60 +1294,42 @@ public final class io/getstream/video/android/compose/ui/components/call/activec public static field lambda-3 Lkotlin/jvm/functions/Function3; public static field lambda-4 Lkotlin/jvm/functions/Function2; public static field lambda-5 Lkotlin/jvm/functions/Function2; - public static field lambda-6 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function6; public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-3$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-4$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-5$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-6$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/video/android/compose/ui/components/call/activecall/internal/ComposableSingletons$InviteUsersDialogKt { public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/activecall/internal/ComposableSingletons$InviteUsersDialogKt; - public static field lambda-1 Lkotlin/jvm/functions/Function3; - public static field lambda-2 Lkotlin/jvm/functions/Function3; + public static field lambda-1 Lkotlin/jvm/functions/Function2; public fun ()V - public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; - public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/video/android/compose/ui/components/call/controls/ComposableSingletons$ControlActionsKt { public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/controls/ComposableSingletons$ControlActionsKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; - public static field lambda-2 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/video/android/compose/ui/components/call/controls/ControlActionsKt { - public static final fun ControlActions--6D8GgA (Lio/getstream/video/android/core/Call;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;JFLandroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/unit/Dp;Ljava/util/List;Landroidx/compose/runtime/Composer;II)V + public static final fun ControlActions (Lio/getstream/video/android/core/Call;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Ljava/util/List;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/video/android/compose/ui/components/call/controls/actions/AcceptCallActionKt { - public static final fun AcceptCallAction (Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/graphics/Shape;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V -} - -public final class io/getstream/video/android/compose/ui/components/call/controls/actions/AudioControlsActionsKt { - public static final fun buildDefaultAudioControlActions (Lio/getstream/video/android/core/Call;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Ljava/util/List; -} - -public final class io/getstream/video/android/compose/ui/components/call/controls/actions/CallControlActionBackgroundKt { - public static final fun CallControlActionBackground-Y0xEhic (Landroidx/compose/ui/Modifier;ZJJLandroidx/compose/ui/graphics/Shape;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V -} - -public final class io/getstream/video/android/compose/ui/components/call/controls/actions/CallControlsActionsKt { - public static final fun buildDefaultCallControlActions (Lio/getstream/video/android/core/Call;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Ljava/util/List; + public static final fun AcceptCallAction-Ks3TJJE (Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/graphics/vector/ImageVector;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/video/android/compose/ui/components/call/controls/actions/CancelCallActionKt { - public static final fun CancelCallAction (Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function1;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/runtime/Composer;II)V + public static final fun CancelCallAction-Ks3TJJE (Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/graphics/vector/ImageVector;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ChatDialogActionKt { - public static final fun ChatDialogAction-jB83MbM (Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/graphics/Shape;JJLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun ChatDialogAction-_8q-z2c (Landroidx/compose/ui/Modifier;ZLjava/lang/Integer;Landroidx/compose/ui/graphics/vector/ImageVector;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$AcceptCallActionKt { @@ -701,6 +1346,13 @@ public final class io/getstream/video/android/compose/ui/components/call/control public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$ChatDialogActionKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$ChatDialogActionKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; +} + public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$DeclineCallActionKt { public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$DeclineCallActionKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; @@ -708,22 +1360,55 @@ public final class io/getstream/video/android/compose/ui/components/call/control public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } -public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$LandscapeControlActionsKt { - public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$LandscapeControlActionsKt; +public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$FlipCameraActionKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$FlipCameraActionKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; +} + +public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$LeaveCallActionKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$LeaveCallActionKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; +} + +public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$SettingsActionKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$SettingsActionKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } -public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$RegularControlActionsKt { - public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$RegularControlActionsKt; +public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$ToggleCameraActionKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$ToggleCameraActionKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$ToggleMicrophoneActionKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$ToggleMicrophoneActionKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; +} + +public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$ToggleSpeakerphoneActionKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/controls/actions/ComposableSingletons$ToggleSpeakerphoneActionKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; +} + +public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ControlActionsBuilderKt { + public static final fun buildDefaultAudioControlActions (Lio/getstream/video/android/core/Call;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Ljava/util/List; + public static final fun buildDefaultCallControlActions (Lio/getstream/video/android/core/Call;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Ljava/util/List; +} + public final class io/getstream/video/android/compose/ui/components/call/controls/actions/DeclineCallActionKt { - public static final fun DeclineCallAction (Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function1;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/runtime/Composer;II)V + public static final fun DeclineCallAction-0ptbMTc (Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function1;Landroidx/compose/ui/graphics/vector/ImageVector;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/video/android/compose/ui/components/call/controls/actions/DefaultOnCallActionHandler { @@ -733,39 +1418,37 @@ public final class io/getstream/video/android/compose/ui/components/call/control } public final class io/getstream/video/android/compose/ui/components/call/controls/actions/FlipCameraActionKt { - public static final fun FlipCameraAction-jB83MbM (Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/graphics/Shape;JJLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun FlipCameraAction-3tXnx4A (Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } -public final class io/getstream/video/android/compose/ui/components/call/controls/actions/LandscapeControlActionsKt { - public static final fun LandscapeControlActions-Z_s7CdY (Lio/getstream/video/android/core/Call;Landroidx/compose/ui/Modifier;JLandroidx/compose/ui/graphics/Shape;FLandroidx/compose/ui/unit/Dp;Lkotlin/jvm/functions/Function1;Ljava/util/List;Landroidx/compose/runtime/Composer;II)V +public final class io/getstream/video/android/compose/ui/components/call/controls/actions/GenericActionsKt { + public static final fun GenericAction-Pz9yvRs (Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/vector/ImageVector;ZLandroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V + public static final fun ToggleAction-X9YjGh4 (Landroidx/compose/ui/Modifier;ZLkotlin/Pair;ZLandroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;ZLio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/video/android/compose/ui/components/call/controls/actions/LeaveCallActionKt { + public static final fun LeaveCalLActionPreview (Landroidx/compose/runtime/Composer;I)V public static final fun LeaveCallAction (Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } -public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ReactionActionKt { - public static final fun ReactionAction-jB83MbM (Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/graphics/Shape;JJLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V -} - -public final class io/getstream/video/android/compose/ui/components/call/controls/actions/RegularControlActionsKt { - public static final fun RegularControlActions-Z_s7CdY (Lio/getstream/video/android/core/Call;Landroidx/compose/ui/Modifier;JLandroidx/compose/ui/graphics/Shape;FLandroidx/compose/ui/unit/Dp;Lkotlin/jvm/functions/Function1;Ljava/util/List;Landroidx/compose/runtime/Composer;II)V -} - public final class io/getstream/video/android/compose/ui/components/call/controls/actions/SettingsActionKt { - public static final fun SettingsAction-jB83MbM (Landroidx/compose/ui/Modifier;ZLandroidx/compose/ui/graphics/Shape;JJLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun ToggleSettingsAction-PBJxc4c (Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun ToggleSettingsActionPreview (Landroidx/compose/runtime/Composer;I)V } public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleCameraActionKt { - public static final fun ToggleCameraAction-md9il-k (Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/graphics/Shape;JJJJLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun ToggleCameraAction-m_EyDiA (Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle;Lio/getstream/video/android/compose/ui/components/base/styling/StreamFixedSizeButtonStyle;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V + public static final fun ToggleCameraActionPreview (Landroidx/compose/runtime/Composer;I)V } public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleMicrophoneActionKt { - public static final fun ToggleMicrophoneAction-md9il-k (Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/graphics/Shape;JJJJLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun ToggleMicrophoneAction-uMBdHk4 (Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun ToggleMicrophoneActionPreview (Landroidx/compose/runtime/Composer;I)V } public final class io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleSpeakerphoneActionKt { - public static final fun ToggleSpeakerphoneAction-md9il-k (Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/graphics/Shape;JJJJLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun ToggleSpeakerphoneAction-uMBdHk4 (Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Landroidx/compose/ui/graphics/Color;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun ToggleSpeakerphoneActionPreview (Landroidx/compose/runtime/Composer;I)V } public final class io/getstream/video/android/compose/ui/components/call/diagnostics/CallDiagnosticsContentKt { @@ -788,7 +1471,7 @@ public final class io/getstream/video/android/compose/ui/components/call/diagnos } public final class io/getstream/video/android/compose/ui/components/call/lobby/CallLobbyKt { - public static final fun CallLobby (Landroidx/compose/ui/Modifier;Lio/getstream/video/android/core/Call;Lio/getstream/video/android/model/User;Landroidx/compose/ui/Alignment;ZZLio/getstream/video/android/core/ParticipantState$Video;Lio/getstream/video/android/compose/permission/VideoPermissionsState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun CallLobby (Landroidx/compose/ui/Modifier;Lio/getstream/video/android/core/Call;Lio/getstream/video/android/model/User;Landroidx/compose/ui/Alignment;ZZLio/getstream/video/android/core/ParticipantState$Video;Lio/getstream/video/android/compose/permission/VideoPermissionsState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/video/android/compose/ui/components/call/lobby/ComposableSingletons$CallLobbyKt { @@ -817,21 +1500,11 @@ public final class io/getstream/video/android/compose/ui/components/call/pinning public static final field $stable I public fun (Landroidx/compose/ui/graphics/vector/ImageVector;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;)V public synthetic fun (Landroidx/compose/ui/graphics/vector/ImageVector;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Landroidx/compose/ui/graphics/vector/ImageVector; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Z - public final fun component4 ()Lkotlin/jvm/functions/Function2; - public final fun component5 ()Lkotlin/jvm/functions/Function3; - public final fun copy (Landroidx/compose/ui/graphics/vector/ImageVector;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;)Lio/getstream/video/android/compose/ui/components/call/pinning/ParticipantAction; - public static synthetic fun copy$default (Lio/getstream/video/android/compose/ui/components/call/pinning/ParticipantAction;Landroidx/compose/ui/graphics/vector/ImageVector;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/call/pinning/ParticipantAction; - public fun equals (Ljava/lang/Object;)Z public final fun getAction ()Lkotlin/jvm/functions/Function3; public final fun getCondition ()Lkotlin/jvm/functions/Function2; public final fun getFirstToggleAction ()Z public final fun getIcon ()Landroidx/compose/ui/graphics/vector/ImageVector; public final fun getLabel ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; } public final class io/getstream/video/android/compose/ui/components/call/renderer/ComposableSingletons$FloatingParticipantVideoKt { @@ -847,15 +1520,17 @@ public final class io/getstream/video/android/compose/ui/components/call/rendere public static field lambda-2 Lkotlin/jvm/functions/Function6; public static field lambda-3 Lkotlin/jvm/functions/Function3; public static field lambda-4 Lkotlin/jvm/functions/Function2; - public static field lambda-5 Lkotlin/jvm/functions/Function2; + public static field lambda-5 Lkotlin/jvm/functions/Function3; public static field lambda-6 Lkotlin/jvm/functions/Function2; + public static field lambda-7 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function4; public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function6; public final fun getLambda-3$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-4$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-5$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-5$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-6$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-7$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/video/android/compose/ui/components/call/renderer/ComposableSingletons$ParticipantsLayoutKt { @@ -1033,11 +1708,9 @@ public final class io/getstream/video/android/compose/ui/components/call/rendere public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/renderer/internal/ComposableSingletons$LandscapeScreenSharingVideoRendererKt; public static field lambda-1 Lkotlin/jvm/functions/Function6; public static field lambda-2 Lkotlin/jvm/functions/Function2; - public static field lambda-3 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function6; public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-3$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/video/android/compose/ui/components/call/renderer/internal/ComposableSingletons$LandscapeVideoRendererKt { @@ -1122,6 +1795,20 @@ public final class io/getstream/video/android/compose/ui/components/call/rendere public final fun getLambda-8$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/video/android/compose/ui/components/call/renderer/internal/ComposableSingletons$ScreenShareTooltipKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/renderer/internal/ComposableSingletons$ScreenShareTooltipKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; +} + +public final class io/getstream/video/android/compose/ui/components/call/renderer/internal/ComposableSingletons$ScreenShareVideoRendererKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/renderer/internal/ComposableSingletons$ScreenShareVideoRendererKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; +} + public final class io/getstream/video/android/compose/ui/components/call/renderer/internal/ComposableSingletons$SpotlightVideorendererKt { public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/call/renderer/internal/ComposableSingletons$SpotlightVideorendererKt; public static field lambda-1 Lkotlin/jvm/functions/Function6; @@ -1236,19 +1923,8 @@ public final class io/getstream/video/android/compose/ui/components/call/ringing public static final fun OutgoingCallDetails (Landroidx/compose/ui/Modifier;ZLjava/util/List;Landroidx/compose/runtime/Composer;II)V } -public final class io/getstream/video/android/compose/ui/components/connection/ComposableSingletons$NetworkQualityIndicatorKt { - public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/connection/ComposableSingletons$NetworkQualityIndicatorKt; - public static field lambda-1 Lkotlin/jvm/functions/Function2; - public fun ()V - public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; -} - -public final class io/getstream/video/android/compose/ui/components/connection/NetworkQualityIndicatorKt { - public static final fun NetworkQualityIndicator (Lio/getstream/video/android/core/model/NetworkQuality;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V -} - public final class io/getstream/video/android/compose/ui/components/indicator/AudioVolumeIndicatorKt { - public static final fun AudioVolumeIndicator (Landroidx/compose/ui/Modifier;FLandroidx/compose/runtime/Composer;II)V + public static final fun AudioVolumeIndicator-FNF3uiM (Landroidx/compose/ui/Modifier;FJLandroidx/compose/runtime/Composer;II)V } public final class io/getstream/video/android/compose/ui/components/indicator/ComposableSingletons$AudioVolumeIndicatorKt { @@ -1278,6 +1954,13 @@ public final class io/getstream/video/android/compose/ui/components/indicator/Co public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/video/android/compose/ui/components/indicator/ComposableSingletons$NetworkQualityIndicatorKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/indicator/ComposableSingletons$NetworkQualityIndicatorKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; +} + public final class io/getstream/video/android/compose/ui/components/indicator/ComposableSingletons$SoundIndicatorKt { public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/indicator/ComposableSingletons$SoundIndicatorKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; @@ -1289,6 +1972,10 @@ public final class io/getstream/video/android/compose/ui/components/indicator/Mi public static final fun MicrophoneIndicator (Landroidx/compose/ui/Modifier;ZLandroidx/compose/runtime/Composer;II)V } +public final class io/getstream/video/android/compose/ui/components/indicator/NetworkQualityIndicatorKt { + public static final fun NetworkQualityIndicator (Lio/getstream/video/android/core/model/NetworkQuality;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + public final class io/getstream/video/android/compose/ui/components/indicator/SoundIndicatorKt { public static final fun SoundIndicator (Landroidx/compose/ui/Modifier;ZZFLandroidx/compose/runtime/Composer;II)V } @@ -1312,8 +1999,10 @@ public final class io/getstream/video/android/compose/ui/components/livestream/C public final class io/getstream/video/android/compose/ui/components/livestream/ComposableSingletons$LivestreamRendererKt { public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/livestream/ComposableSingletons$LivestreamRendererKt; public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayerKt { @@ -1335,35 +2024,18 @@ public final class io/getstream/video/android/compose/ui/components/participants public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } -public final class io/getstream/video/android/compose/ui/components/participants/ComposableSingletons$ParticipantIndicatorKt { - public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/participants/ComposableSingletons$ParticipantIndicatorKt; - public static field lambda-1 Lkotlin/jvm/functions/Function2; - public static field lambda-2 Lkotlin/jvm/functions/Function2; - public fun ()V - public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; -} - -public final class io/getstream/video/android/compose/ui/components/participants/ParticipantIndicatorKt { - public static final fun ParticipantIndicatorIcon-DTcfvLk (IJJZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V -} - public final class io/getstream/video/android/compose/ui/components/participants/internal/ComposableSingletons$CallParticipantListAppBarKt { public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/participants/internal/ComposableSingletons$CallParticipantListAppBarKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; - public static field lambda-2 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/video/android/compose/ui/components/participants/internal/ComposableSingletons$CallParticipantsInfoActionsKt { public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/participants/internal/ComposableSingletons$CallParticipantsInfoActionsKt; - public static field lambda-1 Lkotlin/jvm/functions/Function3; - public static field lambda-2 Lkotlin/jvm/functions/Function2; + public static field lambda-1 Lkotlin/jvm/functions/Function2; public fun ()V - public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; - public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/video/android/compose/ui/components/participants/internal/ComposableSingletons$CallParticipantsListKt { @@ -1405,8 +2077,10 @@ public final class io/getstream/video/android/compose/ui/components/participants public final class io/getstream/video/android/compose/ui/components/video/ComposableSingletons$VideoRendererKt { public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/video/ComposableSingletons$VideoRendererKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; + public static field lambda-2 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/video/android/compose/ui/components/video/VideoRendererKt { diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/VideoTheme.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/VideoTheme.kt index ee163b2489..b362d36194 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/VideoTheme.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/VideoTheme.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId +import io.getstream.video.android.compose.theme.base.StreamRippleTheme import io.getstream.video.android.core.mapper.ReactionMapper /** @@ -38,6 +39,7 @@ import io.getstream.video.android.core.mapper.ReactionMapper private val LocalColors = compositionLocalOf { error("No colors provided! Make sure to wrap all usages of Stream components in a VideoTheme.") } + private val LocalDimens = compositionLocalOf { error("No dimens provided! Make sure to wrap all usages of Stream components in a VideoTheme.") } @@ -68,6 +70,7 @@ private val LocalReactionMapper = compositionLocalOf { * @param reactionMapper Defines a mapper of the emoji code from the reaction events. * @param content The content shown within the theme wrapper. */ +@OptIn(ExperimentalComposeUiApi::class) @Composable public fun VideoTheme( isInDarkMode: Boolean = isSystemInDarkTheme(), diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamColors.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamColors.kt new file mode 100644 index 0000000000..ae16917e07 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamColors.kt @@ -0,0 +1,115 @@ +/* + * 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.compose.theme.base + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import io.getstream.video.android.ui.common.R + +public data class StreamColors( + val brandPrimary: Color, + val brandPrimaryLt: Color, + val brandPrimaryDk: Color, + val brandSecondary: Color, + val brandSecondaryTransparent: Color, + val brandCyan: Color, + val brandGreen: Color, + val brandYellow: Color, + val brandRed: Color, + val brandRedLt: Color, + val brandRedDk: Color, + val brandMaroon: Color, + val brandViolet: Color, + val basePrimary: Color, + val baseSecondary: Color, + val baseTertiary: Color, + val baseQuaternary: Color, + val baseQuinary: Color, + val baseSenary: Color, + val baseSheetPrimary: Color, + val baseSheetSecondary: Color, + val baseSheetTertiary: Color, + val baseSheetQuarternary: Color, + val buttonPrimaryDefault: Color, + val buttonPrimaryPressed: Color, + val buttonPrimaryDisabled: Color, + val buttonBrandDefault: Color, + val buttonBrandPressed: Color, + val buttonBrandDisabled: Color, + val buttonAlertDefault: Color, + val buttonAlertPressed: Color, + val buttonAlertDisabled: Color, + val iconDefault: Color, + val iconPressed: Color, + val iconActive: Color, + val iconAlert: Color, + val iconDisabled: Color, + val alertSuccess: Color, + val alertCaution: Color, + val alertWarning: Color, +) { + public companion object { + @Composable + public fun defaultColors(): StreamColors = StreamColors( + brandPrimary = colorResource(id = R.color.stream_video_brand_primary), + brandPrimaryLt = colorResource(id = R.color.stream_video_brand_primary_lt), + brandPrimaryDk = colorResource(id = R.color.stream_video_brand_primary_dk), + brandSecondary = colorResource(id = R.color.stream_video_brand_secondary), + brandSecondaryTransparent = colorResource( + id = R.color.stream_video_brand_secondary_transparent, + ), + brandCyan = colorResource(id = R.color.stream_video_brand_cyan), + brandGreen = colorResource(id = R.color.stream_video_brand_green), + brandYellow = colorResource(id = R.color.stream_video_brand_yellow), + brandRed = colorResource(id = R.color.stream_video_brand_red), + brandRedLt = colorResource(id = R.color.stream_video_brand_red_lt), + brandRedDk = colorResource(id = R.color.stream_video_brand_red_dk), + brandMaroon = colorResource(id = R.color.stream_video_brand_maroon), + brandViolet = colorResource(id = R.color.stream_video_brand_violet), + basePrimary = colorResource(id = R.color.stream_video_base_primary), + baseSecondary = colorResource(id = R.color.stream_video_base_secondary), + baseTertiary = colorResource(id = R.color.stream_video_base_tetriary), + baseQuaternary = colorResource(id = R.color.stream_video_base_quaternary), + baseQuinary = colorResource(id = R.color.stream_video_base_quinary), + baseSenary = colorResource(id = R.color.stream_video_base_senary), + baseSheetPrimary = colorResource(id = R.color.stream_video_base_sheet_primary), + baseSheetSecondary = colorResource(id = R.color.stream_video_base_sheet_secondary), + baseSheetTertiary = colorResource(id = R.color.stream_video_base_sheet_tetriary), + baseSheetQuarternary = colorResource(id = R.color.stream_video_base_sheet_quaternary), + buttonPrimaryDefault = colorResource(id = R.color.stream_video_button_primary_default), + buttonPrimaryPressed = colorResource(id = R.color.stream_video_button_primary_pressed), + buttonPrimaryDisabled = colorResource( + id = R.color.stream_video_button_primary_disabled, + ), + buttonBrandDefault = colorResource(id = R.color.stream_video_button_brand_default), + buttonBrandPressed = colorResource(id = R.color.stream_video_button_brand_pressed), + buttonBrandDisabled = colorResource(id = R.color.stream_video_button_brand_disabled), + buttonAlertDefault = colorResource(id = R.color.stream_video_button_alert_default), + buttonAlertPressed = colorResource(id = R.color.stream_video_button_alert_pressed), + buttonAlertDisabled = colorResource(id = R.color.stream_video_button_alert_disabled), + iconDefault = colorResource(id = R.color.stream_video_icon_default), + iconPressed = colorResource(id = R.color.stream_video_icon_pressed), + iconActive = colorResource(id = R.color.stream_video_icon_active), + iconAlert = colorResource(id = R.color.stream_video_icon_alert), + iconDisabled = colorResource(id = R.color.stream_video_icon_disabled), + alertSuccess = colorResource(id = R.color.stream_video_alert_success), + alertCaution = colorResource(id = R.color.stream_video_alert_caution), + alertWarning = colorResource(id = R.color.stream_video_alert_warning), + ) + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamDimens.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamDimens.kt new file mode 100644 index 0000000000..6b1e650342 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamDimens.kt @@ -0,0 +1,120 @@ +/* + * 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.compose.theme.base + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import io.getstream.video.android.compose.utils.textSizeResource +import io.getstream.video.android.ui.common.R + +public data class StreamDimens( + val genericMax: Dp, + val generic3xl: Dp, + val genericXxl: Dp, + val genericXl: Dp, + val genericL: Dp, + val genericM: Dp, + val genericS: Dp, + val genericXs: Dp, + val genericXXs: Dp, + val roundnessXl: Dp, + val roundnessL: Dp, + val roundnessM: Dp, + val roundnessS: Dp, + val spacingXl: Dp, + val spacingL: Dp, + val spacingM: Dp, + val spacingS: Dp, + val spacingXs: Dp, + val spacingXXs: Dp, + val componentHeightL: Dp, + val componentHeightM: Dp, + val componentHeightS: Dp, + val componentPaddingTop: Dp, + val componentPaddingStart: Dp, + val componentPaddingBottom: Dp, + val componentPaddingEnd: Dp, + val componentPaddingFixed: Dp, + val textSizeXxl: TextUnit, + val textSizeXl: TextUnit, + val textSizeL: TextUnit, + val textSizeM: TextUnit, + val textSizeS: TextUnit, + val textSizeXs: TextUnit, + val lineHeightXxl: TextUnit, + val lineHeightXl: TextUnit, + val lineHeightL: TextUnit, + val lineHeightM: TextUnit, + val lineHeightS: TextUnit, + val lineHeightXs: TextUnit, +) { + public companion object { + + @Composable + public fun defaultDimens(): StreamDimens { + return StreamDimens( + genericMax = dimensionResource(R.dimen.stream_video_generic_max), + generic3xl = dimensionResource(id = R.dimen.stream_video_generic_3xl), + genericXxl = dimensionResource(id = R.dimen.stream_video_generic_xxl), + genericXl = dimensionResource(R.dimen.stream_video_generic_xl), + genericL = dimensionResource(R.dimen.stream_video_generic_l), + genericM = dimensionResource(R.dimen.stream_video_generic_m), + genericS = dimensionResource(R.dimen.stream_video_generic_s), + genericXs = dimensionResource(R.dimen.stream_video_generic_xs), + genericXXs = dimensionResource(R.dimen.stream_video_generic_xxs), + roundnessXl = dimensionResource(R.dimen.stream_video_roundness_xl), + roundnessL = dimensionResource(R.dimen.stream_video_roundness_l), + roundnessM = dimensionResource(R.dimen.stream_video_roundness_m), + roundnessS = dimensionResource(R.dimen.stream_video_roundness_s), + spacingXl = dimensionResource(R.dimen.stream_video_spacing_xl), + spacingL = dimensionResource(R.dimen.stream_video_spacing_l), + spacingM = dimensionResource(R.dimen.stream_video_spacing_m), + spacingS = dimensionResource(R.dimen.stream_video_spacing_s), + spacingXs = dimensionResource(R.dimen.stream_video_spacing_xs), + spacingXXs = dimensionResource(R.dimen.stream_video_spacing_xxs), + componentHeightL = dimensionResource(R.dimen.stream_video_component_height_l), + componentHeightM = dimensionResource(R.dimen.stream_video_component_height_m), + componentHeightS = dimensionResource(R.dimen.stream_video_component_height_s), + componentPaddingTop = dimensionResource(R.dimen.stream_video_component_padding_top), + componentPaddingStart = dimensionResource( + R.dimen.stream_video_component_padding_start, + ), + componentPaddingBottom = dimensionResource( + R.dimen.stream_video_component_padding_bottom, + ), + componentPaddingEnd = dimensionResource(R.dimen.stream_video_component_padding_end), + componentPaddingFixed = dimensionResource( + R.dimen.stream_video_component_padding_fixed, + ), + textSizeXxl = textSizeResource(R.dimen.stream_video_text_size_xxl), + textSizeXl = textSizeResource(R.dimen.stream_video_text_size_xl), + textSizeL = textSizeResource(R.dimen.stream_video_text_size_l), + textSizeM = textSizeResource(R.dimen.stream_video_text_size_m), + textSizeS = textSizeResource(R.dimen.stream_video_text_size_s), + textSizeXs = textSizeResource(R.dimen.stream_video_text_size_xs), + lineHeightXxl = textSizeResource(R.dimen.stream_video_line_height_xxl), + lineHeightXl = textSizeResource(R.dimen.stream_video_line_height_xl), + lineHeightL = textSizeResource(R.dimen.stream_video_line_height_l), + lineHeightM = textSizeResource(R.dimen.stream_video_line_height_m), + lineHeightS = textSizeResource(R.dimen.stream_video_line_height_s), + lineHeightXs = textSizeResource(R.dimen.stream_video_line_height_xs), + ) + } + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamRippleTheme.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamRippleTheme.kt similarity index 96% rename from stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamRippleTheme.kt rename to stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamRippleTheme.kt index 55427cd27c..f6ad08b8db 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamRippleTheme.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamRippleTheme.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.getstream.video.android.compose.theme +package io.getstream.video.android.compose.theme.base import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.LocalContentColor diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamShapes.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamShapes.kt new file mode 100644 index 0000000000..110c90ba25 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamShapes.kt @@ -0,0 +1,68 @@ +/* + * 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.compose.theme.base + +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.CutCornerShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp + +/** + * Contains all the shapes we provide for our components. + * + * @param avatar Used for avatar UIs in the SDK. + * @param dialog Used for dialog UIs in the SDK, such as user invites. + * @param callButton The shape of call buttons. + * @param callControls The shape of the call controls sheet when in a call. + * @param callControlsButton Tha shape of the buttons within Call Controls. + * @param participantsInfoMenuButton The shape of buttons in the Participants Info menu. + * @param indicatorBackground The indicator background shape. + */ +@Immutable +public data class StreamShapes( + public val circle: Shape, + public val square: Shape, + public val button: Shape, + public val input: Shape, + public val dialog: Shape, + public val sheet: Shape, + public val indicator: Shape, + public val container: Shape, +) { + public companion object { + /** + * Builds the default shapes for our theme. + * + * @return A [StreamShapes] that holds our default shapes. + */ + @Composable + public fun defaultShapes(dimens: StreamDimens): StreamShapes = StreamShapes( + circle = CircleShape, + button = RoundedCornerShape(dimens.roundnessXl), + input = RoundedCornerShape(dimens.roundnessXl), + sheet = RoundedCornerShape(dimens.roundnessM), + dialog = RoundedCornerShape(dimens.roundnessL), + container = RoundedCornerShape(dimens.roundnessXl), + indicator = RoundedCornerShape(dimens.roundnessS), + square = CutCornerShape(CornerSize(0.dp)), + ) + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamTypography.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamTypography.kt new file mode 100644 index 0000000000..6c0a64cbbb --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/StreamTypography.kt @@ -0,0 +1,173 @@ +/* + * 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.compose.theme.base + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +/** + * Contains all the typography we provide for our components. + * + * @param title1 Used for big titles, like the image attachment overlay text. + * @param title3 Used for empty content text. + * @param title3Bold Used for titles of app bars and bottom bars. + * @param body Used for body content, such as messages. + * @param bodyItalic Used for body content, italicized, like deleted message components. + * @param bodyBold Used for emphasized body content, like small titles. + * @param footnote Used for footnote information, like timestamps. + * @param footnoteItalic Used for footnote information that's less important, like the deleted message text. + * @param footnoteBold Used for footnote information in certain important items, like the thread reply text, + * or user info components. + * @param captionBold Used for unread count indicator. + */ +@Immutable +public data class StreamTypography( + public val titleL: TextStyle, + public val titleM: TextStyle, + public val titleS: TextStyle, + public val titleXs: TextStyle, + public val subtitleL: TextStyle, + public val subtitleM: TextStyle, + public val subtitleS: TextStyle, + public val bodyL: TextStyle, + public val bodyM: TextStyle, + public val bodyS: TextStyle, + public val labelL: TextStyle, + public val labelM: TextStyle, + public val labelS: TextStyle, + public val labelXS: TextStyle, +) { + + public companion object { + /** + * Builds the default typography set for our theme, with the ability to customize the font family. + * + * @param fontFamily The font that the users want to use for the app. + * @return [StreamTypography] that holds all the default text styles that we support. + */ + @Composable + public fun defaultTypography( + colors: StreamColors, + dimens: StreamDimens, + fontFamily: FontFamily? = null, + ): StreamTypography = StreamTypography( + titleL = TextStyle( + fontSize = dimens.textSizeXxl, + lineHeight = dimens.lineHeightXxl, + fontFamily = fontFamily, + fontWeight = FontWeight.W500, + color = colors.basePrimary, + ), + titleM = TextStyle( + fontSize = dimens.textSizeXl, + lineHeight = dimens.lineHeightXl, + fontFamily = fontFamily, + fontWeight = FontWeight.W500, + color = colors.basePrimary, + ), + titleS = TextStyle( + fontSize = dimens.textSizeL, + lineHeight = dimens.lineHeightL, + fontFamily = fontFamily, + fontWeight = FontWeight.W500, + color = colors.basePrimary, + ), + subtitleL = TextStyle( + fontSize = dimens.textSizeL, + lineHeight = dimens.lineHeightM, + fontFamily = fontFamily, + fontWeight = FontWeight.W500, + color = colors.baseTertiary, + ), + subtitleM = TextStyle( + fontSize = dimens.textSizeM, + lineHeight = dimens.lineHeightS, + fontFamily = fontFamily, + fontWeight = FontWeight.W500, + color = colors.baseTertiary, + ), + subtitleS = TextStyle( + fontSize = dimens.textSizeS, + lineHeight = dimens.lineHeightXs, + fontFamily = fontFamily, + fontWeight = FontWeight.W500, + color = colors.baseTertiary, + ), + titleXs = TextStyle( + fontSize = dimens.textSizeXs, + lineHeight = dimens.lineHeightM, + fontFamily = fontFamily, + fontWeight = FontWeight.W600, + color = colors.baseQuinary, + letterSpacing = 1.sp, + ), + bodyL = TextStyle( + fontSize = dimens.textSizeM, + lineHeight = dimens.lineHeightXl, + fontFamily = fontFamily, + fontWeight = FontWeight.W400, + color = colors.baseQuinary, + ), + bodyM = TextStyle( + fontSize = dimens.textSizeS, + lineHeight = dimens.lineHeightL, + fontFamily = fontFamily, + fontWeight = FontWeight.W400, + color = colors.baseQuinary, + ), + bodyS = TextStyle( + fontSize = dimens.textSizeXs, + lineHeight = dimens.lineHeightM, + fontFamily = fontFamily, + fontWeight = FontWeight.W400, + color = colors.baseQuinary, + ), + labelL = TextStyle( + fontSize = dimens.textSizeM, + lineHeight = dimens.lineHeightL, + fontFamily = fontFamily, + fontWeight = FontWeight.W600, + color = colors.basePrimary, + ), + labelM = TextStyle( + fontSize = dimens.textSizeS, + lineHeight = dimens.lineHeightL, + fontFamily = fontFamily, + fontWeight = FontWeight.W600, + color = colors.basePrimary, + ), + labelS = TextStyle( + fontSize = dimens.textSizeXs, + lineHeight = dimens.lineHeightS, + fontFamily = fontFamily, + fontWeight = FontWeight.W500, + color = colors.basePrimary, + ), + labelXS = TextStyle( + fontSize = dimens.textSizeXs, + lineHeight = dimens.lineHeightXs, + fontFamily = fontFamily, + fontWeight = FontWeight.W500, + color = colors.basePrimary, + ), + ) + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/VideoTheme.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/VideoTheme.kt new file mode 100644 index 0000000000..510388fc92 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/base/VideoTheme.kt @@ -0,0 +1,166 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalComposeUiApi::class) + +package io.getstream.video.android.compose.theme.base + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.material.ripple.LocalRippleTheme +import androidx.compose.material.ripple.RippleTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import io.getstream.video.android.compose.ui.components.base.styling.CompositeStyleProvider +import io.getstream.video.android.core.mapper.ReactionMapper + +/** + * Local providers for various properties we connect to our components, for styling. + */ +private val LocalColors = compositionLocalOf { + error("No colors provided! Make sure to wrap all usages of Stream components in a VideoTheme.") +} + +private val LocalDimens = compositionLocalOf { + error("No dimens provided! Make sure to wrap all usages of Stream components in a VideoTheme.") +} +private val LocalTypography = compositionLocalOf { + error( + "No typography provided! Make sure to wrap all usages of Stream components in a VideoTheme.", + ) +} +private val LocalShapes = compositionLocalOf { + error("No shapes provided! Make sure to wrap all usages of Stream components in a VideoTheme.") +} +private val LocalReactionMapper = compositionLocalOf { + error( + "No reaction mapper provided! Make sure to wrap all usages of Stream components in a VideoTheme.", + ) +} + +private val LocalStyles = compositionLocalOf { + error( + "No styles provided! Make sure to wrap all usages of Stream components in a VideoTheme.", + ) +} + +/** + * Our theme that provides all the important properties for styling to the user. + * + * @param isInDarkMode If we're currently in the dark mode or not. Affects only the default color palette that's + * provided. If you customize [colors], make sure to add your own logic for dark/light colors. + * @param colors The set of colors we provide, wrapped in [StreamColors]. + * @param dimens The set of dimens we provide, wrapped in [StreamDimens]. + * @param typography The set of typography styles we provide, wrapped in [StreamTypography]. + * @param shapes The set of shapes we provide, wrapped in [StreamShapes]. + * @param rippleTheme Defines the appearance for ripples. + * @param reactionMapper Defines a mapper of the emoji code from the reaction events. + * @param content The content shown within the theme wrapper. + */ +@Composable +public fun VideoTheme( + isInDarkMode: Boolean = isSystemInDarkTheme(), + colors: StreamColors = StreamColors.defaultColors(), + dimens: StreamDimens = StreamDimens.defaultDimens(), + typography: StreamTypography = StreamTypography.defaultTypography(colors, dimens), + shapes: StreamShapes = StreamShapes.defaultShapes(dimens), + rippleTheme: RippleTheme = StreamRippleTheme, + reactionMapper: ReactionMapper = ReactionMapper.defaultReactionMapper(), + allowUIAutomationTest: Boolean = true, + styles: CompositeStyleProvider = CompositeStyleProvider(), + content: @Composable () -> Unit, +) { + CompositionLocalProvider( + LocalColors provides colors, + LocalDimens provides dimens, + LocalTypography provides typography, + LocalShapes provides shapes, + LocalRippleTheme provides rippleTheme, + LocalReactionMapper provides reactionMapper, + LocalStyles provides styles, + ) { + Box( + modifier = Modifier.semantics { + testTagsAsResourceId = allowUIAutomationTest + }, + ) { + content() + } + } +} + +public interface StreamTheme { + /** + * Retrieves the current [StreamColors] at the call site's position in the hierarchy. + */ + public val colors: StreamColors + @Composable @ReadOnlyComposable + get() = LocalColors.current + + /** + * Retrieves the current [StreamDimens] at the call site's position in the hierarchy. + */ + public val dimens: StreamDimens + @Composable @ReadOnlyComposable + get() = LocalDimens.current + + /** + * Retrieves the current [StreamTypography] at the call site's position in the hierarchy. + */ + public val typography: StreamTypography + @Composable @ReadOnlyComposable + get() = LocalTypography.current + + /** + * Retrieves the current [StreamShapes] at the call site's position in the hierarchy. + */ + public val shapes: StreamShapes + @Composable @ReadOnlyComposable + get() = LocalShapes.current + + /** + * Retrieves the current [RippleTheme] at the call site's position in the hierarchy. + */ + public val rippleTheme: RippleTheme + @Composable @ReadOnlyComposable + get() = LocalRippleTheme.current + + /** + * Retrieves the current [ReactionMapper] at the call site's position in the hierarchy. + */ + public val reactionMapper: ReactionMapper + @Composable @ReadOnlyComposable + get() = LocalReactionMapper.current + + /** + * Retrieves the current [ReactionMapper] at the call site's position in the hierarchy. + */ + public val styles: CompositeStyleProvider + @Composable @ReadOnlyComposable + get() = LocalStyles.current +} + +/** + * Contains ease-of-use accessors for different properties used to style and customize the app + * look and feel. + */ +public object VideoTheme : StreamTheme diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioAppBar.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioAppBar.kt index ed1c48c3cd..f56cb36a32 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioAppBar.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioAppBar.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme /** * Represents the default AppBar that's shown in the audio room. @@ -41,12 +41,12 @@ public fun AudioAppBar( modifier: Modifier = Modifier, title: String, ) { - Column(modifier) { + Column(modifier.background(VideoTheme.colors.baseSheetSecondary)) { Text( text = title, fontSize = 20.sp, fontWeight = FontWeight.Bold, - color = VideoTheme.colors.textHighEmphasis, + color = VideoTheme.colors.basePrimary, ) Spacer(modifier = Modifier.height(18.dp)) @@ -55,7 +55,7 @@ public fun AudioAppBar( modifier = Modifier .fillMaxWidth() .height(1.dp) - .background(VideoTheme.colors.borders), + .background(VideoTheme.colors.basePrimary), ) } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioControlActions.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioControlActions.kt index cf84cb1b9d..6e9e96ef7b 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioControlActions.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioControlActions.kt @@ -17,32 +17,25 @@ package io.getstream.video.android.compose.ui.components.audio import androidx.activity.ComponentActivity -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExitToApp import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -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.base.StreamButton import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleMicrophoneAction import io.getstream.video.android.core.Call import io.getstream.video.android.mock.StreamPreviewDataUtils @@ -61,60 +54,34 @@ public fun AudioControlActions( call: Call, modifier: Modifier = Modifier, onLeaveRoom: (() -> Unit)? = null, -) { +): Unit = Box(modifier = modifier) { val isMicrophoneEnabled by if (LocalInspectionMode.current) { remember { mutableStateOf(true) } } else { call.microphone.isEnabled.collectAsStateWithLifecycle() } val activity = LocalContext.current as? ComponentActivity + StreamButton( + text = stringResource( + id = io.getstream.video.android.ui.common.R.string.stream_video_audio_leave, + ), + icon = Icons.Default.ExitToApp, + style = VideoTheme.styles.buttonStyles.secondaryButtonStyle(), + onClick = { + onLeaveRoom?.invoke() ?: let { + call.leave() + activity?.onBackPressedDispatcher?.onBackPressed() + } + }, + ) - Box(modifier = modifier) { - Button( - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .align(Alignment.CenterStart), - onClick = { - onLeaveRoom?.invoke() ?: let { - call.leave() - activity?.onBackPressedDispatcher?.onBackPressed() - } - }, - colors = ButtonDefaults.buttonColors( - backgroundColor = VideoTheme.colors.primaryAccent, - contentColor = VideoTheme.colors.primaryAccent, - ), - ) { - Image( - painter = painterResource( - id = io.getstream.video.android.ui.common.R.drawable.stream_video_ic_leave, - ), - contentDescription = null, - ) - - Spacer(modifier = Modifier.width(10.dp)) - - Text( - text = stringResource( - id = io.getstream.video.android.ui.common.R.string.stream_video_audio_leave, - ), - color = VideoTheme.colors.audioLeaveButton, - fontSize = 16.sp, - ) - } - - ToggleMicrophoneAction( - modifier = Modifier - .align(Alignment.CenterEnd) - .size(45.dp), - isMicrophoneEnabled = isMicrophoneEnabled, - enabledColor = VideoTheme.colors.callActionIconEnabledBackground, - disabledColor = VideoTheme.colors.callActionIconEnabledBackground, - disabledIconTint = VideoTheme.colors.errorAccent, - shape = RoundedCornerShape(8.dp), - onCallAction = { callAction -> call.microphone.setEnabled(callAction.isEnabled) }, - ) - } + ToggleMicrophoneAction( + modifier = Modifier + .align(Alignment.CenterEnd) + .size(45.dp), + isMicrophoneEnabled = isMicrophoneEnabled, + onCallAction = { callAction -> call.microphone.setEnabled(callAction.isEnabled) }, + ) } @Preview diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioParticipantsGrid.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioParticipantsGrid.kt index a495c11a5f..efe34ecc4e 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioParticipantsGrid.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioParticipantsGrid.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.ParticipantState import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewParticipantsList @@ -62,13 +62,13 @@ public fun AudioParticipantsGrid( LazyVerticalGrid( modifier = modifier, - columns = GridCells.Adaptive(VideoTheme.dimens.audioAvatarSize), + columns = GridCells.Adaptive(VideoTheme.dimens.genericMax), contentPadding = PaddingValues(vertical = 32.dp), verticalArrangement = Arrangement.spacedBy( - VideoTheme.dimens.audioRoomAvatarLandscapePadding, + VideoTheme.dimens.spacingM, ), horizontalArrangement = if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - Arrangement.spacedBy(VideoTheme.dimens.audioRoomAvatarLandscapePadding) + Arrangement.spacedBy(VideoTheme.dimens.spacingM) } else { Arrangement.spacedBy(0.dp) }, diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRendererStyle.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRendererStyle.kt index 534af117dd..c79648daa9 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRendererStyle.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRendererStyle.kt @@ -19,7 +19,6 @@ package io.getstream.video.android.compose.ui.components.audio import androidx.compose.foundation.BorderStroke import androidx.compose.runtime.Stable import androidx.compose.ui.Alignment -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -80,12 +79,7 @@ public data class RegularAudioRendererStyle( override val isShowingSpeakingBorder: Boolean = true, override val speakingBorder: BorderStroke = BorderStroke( 2.dp, - Brush.verticalGradient( - listOf( - Color(0xFF005FFF), - Color(0xFF00B2FF), - ), - ), + Color(0xFF005FFF), ), override val isShowingMicrophoneAvailability: Boolean = true, override val microphoneLabelPosition: Alignment = Alignment.BottomEnd, diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRoomContent.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRoomContent.kt index 5f4516dd71..13aa7edcea 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRoomContent.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRoomContent.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.getstream.video.android.compose.permission.VideoPermissionsState import io.getstream.video.android.compose.permission.rememberMicrophonePermissionState -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 io.getstream.video.android.core.ParticipantState import io.getstream.video.android.mock.StreamPreviewDataUtils @@ -109,9 +109,9 @@ public fun AudioRoomContent( Scaffold( modifier = modifier - .background(VideoTheme.colors.appBackground) + .background(VideoTheme.colors.baseSheetPrimary) .padding(32.dp), - contentColor = VideoTheme.colors.appBackground, + contentColor = VideoTheme.colors.baseSheetPrimary, topBar = { if (isShowingAppBar) { appBarContent.invoke(call) @@ -121,7 +121,7 @@ public fun AudioRoomContent( content = { paddings -> Box( modifier = Modifier - .background(color = VideoTheme.colors.appBackground) + .background(color = VideoTheme.colors.baseSheetPrimary) .padding(paddings), ) { audioContent.invoke(this, call) diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/ParticipantAudio.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/ParticipantAudio.kt index 2e4f9be59b..911f71d09a 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/ParticipantAudio.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/ParticipantAudio.kt @@ -33,13 +33,14 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MicOff import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -47,12 +48,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp 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.avatar.UserAvatar import io.getstream.video.android.core.ParticipantState import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewParticipant -import io.getstream.video.android.ui.common.R /** * Renders a single participant with a given call, which displays an avatar of the participant. @@ -84,13 +84,13 @@ public fun ParticipantAudio( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - Box(modifier = Modifier.size(VideoTheme.dimens.audioAvatarSize)) { + Box(modifier = Modifier.fillMaxSize()) { UserAvatar( userName = nameOrId, userImage = userImage, modifier = Modifier .fillMaxSize() - .padding(VideoTheme.dimens.audioAvatarPadding), + .padding(VideoTheme.dimens.spacingM), ) if (isSpeaking && style.isShowingSpeakingBorder) { @@ -103,7 +103,7 @@ public fun ParticipantAudio( Box( modifier = Modifier .fillMaxSize() - .padding(VideoTheme.dimens.audioAvatarPadding), + .padding(VideoTheme.dimens.spacingM), ) { microphoneIndicatorContent.invoke(this, participant) } @@ -117,7 +117,7 @@ public fun ParticipantAudio( text = nameOrId, fontSize = 14.sp, fontWeight = FontWeight.Bold, - color = VideoTheme.colors.textHighEmphasis, + color = VideoTheme.colors.basePrimary, textAlign = TextAlign.Center, overflow = TextOverflow.Ellipsis, maxLines = 1, @@ -137,7 +137,7 @@ public fun ParticipantAudio( modifier = Modifier.fillMaxWidth(), text = roles.firstOrNull().orEmpty(), fontSize = 11.sp, - color = VideoTheme.colors.textHighEmphasis, + color = VideoTheme.colors.basePrimary, textAlign = TextAlign.Center, overflow = TextOverflow.Ellipsis, maxLines = 1, @@ -153,16 +153,15 @@ private fun BoxScope.DefaultMicrophoneIndicator( Box( modifier = Modifier .clip(CircleShape) - .background(VideoTheme.colors.appBackground) - .size(VideoTheme.dimens.audioRoomMicSize) + .background(VideoTheme.colors.baseSheetPrimary) + .size(VideoTheme.dimens.componentHeightM) .align(alignment), ) { Icon( - modifier = Modifier - .fillMaxSize() - .padding(VideoTheme.dimens.audioRoomMicPadding), - painter = painterResource(id = R.drawable.stream_video_ic_mic_off), - tint = VideoTheme.colors.errorAccent, + + modifier = Modifier.align(alignment).fillMaxSize().padding(VideoTheme.dimens.spacingS), + imageVector = Icons.Default.MicOff, + tint = VideoTheme.colors.alertWarning, contentDescription = null, ) } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/Avatar.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/Avatar.kt index f526b43fb1..ec163b425b 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/Avatar.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/Avatar.kt @@ -43,7 +43,8 @@ import com.skydoves.landscapist.animation.crossfade.CrossfadePlugin import com.skydoves.landscapist.coil.CoilImage import com.skydoves.landscapist.components.rememberImageComponent import com.skydoves.landscapist.placeholder.placeholder.PlaceholderPlugin -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.styling.StyleSize import io.getstream.video.android.ui.common.R /** @@ -68,11 +69,15 @@ public fun Avatar( modifier: Modifier = Modifier, imageUrl: String? = null, initials: String? = null, - shape: Shape = VideoTheme.shapes.avatar, - textStyle: TextStyle = VideoTheme.typography.title3Bold, + shape: Shape = VideoTheme.shapes.circle, + textSize: StyleSize = StyleSize.XL, + textStyle: TextStyle = VideoTheme.typography.titleM, contentScale: ContentScale = ContentScale.Crop, contentDescription: String? = null, - requestSize: IntSize = IntSize(DEFAULT_IMAGE_SIZE, DEFAULT_IMAGE_SIZE), + requestSize: IntSize = IntSize( + DEFAULT_IMAGE_SIZE, + DEFAULT_IMAGE_SIZE, + ), @DrawableRes previewPlaceholder: Int = LocalAvatarPreviewProvider.getLocalAvatarPreviewPlaceholder(), @DrawableRes loadingPlaceholder: Int? = @@ -97,9 +102,9 @@ public fun Avatar( InitialsAvatar( modifier = modifier, initials = initials, + textSize = textSize, shape = shape, textStyle = textStyle, - avatarOffset = initialsAvatarOffset, ) return } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/InitialsAvatar.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/InitialsAvatar.kt index 1dbe1570a7..6cedbc032e 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/InitialsAvatar.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/InitialsAvatar.kt @@ -18,8 +18,12 @@ package io.getstream.video.android.compose.ui.components.avatar import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -30,8 +34,9 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.compose.utils.initialsGradient +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize +import io.getstream.video.android.compose.utils.initialsColors import io.getstream.video.android.core.utils.initials /** @@ -49,25 +54,35 @@ import io.getstream.video.android.core.utils.initials internal fun InitialsAvatar( initials: String, modifier: Modifier = Modifier, - shape: Shape = VideoTheme.shapes.avatar, - textStyle: TextStyle = VideoTheme.typography.title3Bold, + shape: Shape = VideoTheme.shapes.circle, + textSize: StyleSize = StyleSize.XL, + textStyle: TextStyle = VideoTheme.typography.titleM, avatarOffset: DpOffset = DpOffset(0.dp, 0.dp), initialTransformer: (String) -> String = { it.initials() }, ) { - val initialsGradient = initialsGradient(initials = initials) - + val colors = initialsColors(initials = initials) Box( modifier = modifier + .widthIn(VideoTheme.dimens.genericS, VideoTheme.dimens.genericMax) + .aspectRatio(1f) .clip(shape) - .background(brush = initialsGradient), + .background(color = colors.second), ) { + val resolvedTextSize = when (textSize) { + StyleSize.L -> VideoTheme.dimens.textSizeL + StyleSize.XS -> VideoTheme.dimens.textSizeXs + StyleSize.S -> VideoTheme.dimens.textSizeS + StyleSize.M -> VideoTheme.dimens.textSizeM + StyleSize.XL -> VideoTheme.dimens.textSizeXl + StyleSize.XXL -> VideoTheme.dimens.textSizeXxl + } Text( modifier = Modifier .align(Alignment.Center) .offset(avatarOffset.x, avatarOffset.y), text = initialTransformer.invoke(initials), - style = textStyle, - color = VideoTheme.colors.avatarInitials, + fontSize = resolvedTextSize, + style = textStyle.copy(color = colors.first), ) } } @@ -76,9 +91,23 @@ internal fun InitialsAvatar( @Composable private fun InitialsAvatarPreview() { VideoTheme { - Avatar( - modifier = Modifier.size(56.dp), - initials = "Jaewoong Eum", - ) + Column { + Avatar( + initials = "Jaewoong Eum", + ) + Spacer(modifier = Modifier.size(24.dp)) + Avatar( + initials = "Aleksandar Apostolov", + ) + Spacer(modifier = Modifier.size(24.dp)) + Avatar( + initials = "Danie", + ) + Spacer(modifier = Modifier.size(24.dp)) + Avatar( + initials = "Jaewoong Eum", + ) + Spacer(modifier = Modifier.size(24.dp)) + } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/OnlineIndicator.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/OnlineIndicator.kt index 2633005098..f7757f80d1 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/OnlineIndicator.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/OnlineIndicator.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme /** * Component that represents an online indicator to be used with @@ -37,8 +37,8 @@ public fun OnlineIndicator(modifier: Modifier = Modifier) { Box( modifier = modifier .size(12.dp) - .background(VideoTheme.colors.appBackground, CircleShape) + .background(VideoTheme.colors.baseSheetPrimary, CircleShape) .padding(2.dp) - .background(VideoTheme.colors.infoAccent, CircleShape), + .background(VideoTheme.colors.alertSuccess, CircleShape), ) } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatar.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatar.kt index 810ffad0dd..2909dd0297 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatar.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatar.kt @@ -33,7 +33,8 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewParticipantsList import io.getstream.video.android.model.User @@ -64,10 +65,10 @@ public fun UserAvatar( userName: String?, userImage: String?, modifier: Modifier = Modifier, - shape: Shape = VideoTheme.shapes.avatar, - textStyle: TextStyle = VideoTheme.typography.title3Bold, + shape: Shape = VideoTheme.shapes.circle, contentScale: ContentScale = ContentScale.Crop, contentDescription: String? = null, + textSize: StyleSize = StyleSize.XL, requestSize: IntSize = IntSize(DEFAULT_IMAGE_SIZE, DEFAULT_IMAGE_SIZE), @DrawableRes previewPlaceholder: Int = LocalAvatarPreviewProvider.getLocalAvatarPreviewPlaceholder(), @DrawableRes loadingPlaceholder: Int? = LocalAvatarPreviewProvider.getLocalAvatarLoadingPlaceholder(), @@ -82,9 +83,9 @@ public fun UserAvatar( Box(modifier = modifier) { Avatar( modifier = Modifier.fillMaxSize(), + textSize = textSize, imageUrl = userImage, initials = userName, - textStyle = textStyle, shape = shape, contentScale = contentScale, contentDescription = contentDescription, @@ -115,7 +116,6 @@ private fun UserAvatarPreview() { StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) VideoTheme { val participant = previewParticipantsList[0] - val userId by participant.userId.collectAsStateWithLifecycle() val userImage by participant.image.collectAsStateWithLifecycle() val userName by participant.userNameOrId.collectAsStateWithLifecycle() diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatarBackground.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatarBackground.kt index df5849a1de..935b01540c 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatarBackground.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatarBackground.kt @@ -17,55 +17,44 @@ package io.getstream.video.android.compose.ui.components.avatar import androidx.annotation.DrawableRes -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewUsers /** * A background that displays a user avatar and a background that reflects the avatar. * - * @param user The user whose avatar we want to show. * @param modifier Modifier for styling. * @param shape The shape of the avatar. * @param avatarSize The size to decide avatar image. - * @param avatarShadowElevation The shadow elevation for the avatar image. - * @param textStyle The [TextStyle] that will be used for the initials. * @param contentScale The scale option used for the content. * @param contentDescription The content description of the avatar. * @param requestSize The actual request size. * @param initialsAvatarOffset The initials offset to apply to the avatar. * @param previewPlaceholder A placeholder that will be displayed on the Compose preview (IDE). * @param loadingPlaceholder A placeholder that will be displayed while loading an image. - * @param blurRadius A blur radius value to be applied on the background. */ @Composable public fun UserAvatarBackground( userName: String?, userImage: String?, modifier: Modifier = Modifier, - shape: Shape = VideoTheme.shapes.avatar, - avatarSize: Dp = 84.dp, - avatarShadowElevation: Dp = 12.dp, - textStyle: TextStyle = VideoTheme.typography.title3Bold, + shape: Shape = VideoTheme.shapes.circle, + avatarSize: Dp = VideoTheme.dimens.genericMax, contentScale: ContentScale = ContentScale.Crop, contentDescription: String? = null, requestSize: IntSize = IntSize(DEFAULT_IMAGE_SIZE, DEFAULT_IMAGE_SIZE), @@ -77,22 +66,12 @@ public fun UserAvatarBackground( Box( modifier = Modifier .size(avatarSize) - .align(Alignment.Center) - .border( - width = VideoTheme.dimens.avatarBorderWidth, - color = VideoTheme.colors.avatarBorderColor, - shape = VideoTheme.shapes.avatar, - ), + .align(Alignment.Center), ) { UserAvatar( - modifier = Modifier - .padding(VideoTheme.dimens.avatarBorderPadding) - .align(Alignment.Center) - .shadow(elevation = avatarShadowElevation, shape = CircleShape), userName = userName, userImage = userImage, shape = shape, - textStyle = textStyle, contentScale = contentScale, contentDescription = contentDescription, requestSize = requestSize, diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/background/CallBackground.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/background/CallBackground.kt index cc0bc2c75c..c97db91946 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/background/CallBackground.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/background/CallBackground.kt @@ -21,148 +21,32 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.blur -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.skydoves.landscapist.ImageOptions -import com.skydoves.landscapist.animation.crossfade.CrossfadePlugin -import com.skydoves.landscapist.coil.CoilImage -import com.skydoves.landscapist.components.rememberImageComponent -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.AvatarImagePreview -import io.getstream.video.android.compose.ui.components.plugins.BlurTransformationPlugin -import io.getstream.video.android.core.MemberState -import io.getstream.video.android.core.model.CallUser -import io.getstream.video.android.core.utils.toCallUser import io.getstream.video.android.mock.StreamPreviewDataUtils -import io.getstream.video.android.mock.previewMemberListState -import io.getstream.video.android.ui.common.R /** * Renders a call background that shows either a static image or user images based on the call state. * - * @param participants The list of participants in the call. - * @param isVideoType The type of call, Audio or Video. - * @param isIncoming If the call is incoming from other users or if we're calling other people. * @param modifier Modifier for styling. * @param content The content to render on top of the background. */ @Composable public fun CallBackground( modifier: Modifier = Modifier, - participants: List, - isVideoType: Boolean = true, - isIncoming: Boolean, content: @Composable BoxScope.() -> Unit, ) { - val callUser by remember( - participants, - ) { derivedStateOf { participants.map { it.toCallUser() } } } - - Box(modifier = modifier) { - if (isIncoming) { - IncomingCallBackground(callUser) - } else { - OutgoingCallBackground(callUser, isVideoType) - } - - content() - } -} - -@Composable -private fun IncomingCallBackground(callUsers: List) { - if (callUsers.size == 1) { - ParticipantImageBackground( - userImage = callUsers.first().imageUrl, - ) - } else { - DefaultCallBackground() - } -} - -@Composable -private fun OutgoingCallBackground(callUsers: List, isVideoType: Boolean) { - if (!isVideoType) { - if (callUsers.size == 1) { - ParticipantImageBackground(callUsers.first().imageUrl) - } else { - DefaultCallBackground() - } - } else { - if (callUsers.isNotEmpty()) { - ParticipantImageBackground(userImage = callUsers.first().imageUrl) - } else { - DefaultCallBackground() - } - } -} - -/** - * A background that displays a different background depending on the [userImage]. - * If the [userImage] is valid, the background will display a blurred user image. - * If the [userImage] is invalid, the background will display a gradient color. - * - * @param userImage A user image that will be blurred for the background. - * @param modifier Modifier for styling. - * @param blurRadius A blur radius value to be applied on the background. - */ -@Composable -public fun ParticipantImageBackground( - userImage: String?, - modifier: Modifier = Modifier, - blurRadius: Int = 20, -) { - if (!userImage.isNullOrEmpty()) { - CoilImage( - modifier = - if (LocalInspectionMode.current) { - modifier - .fillMaxSize() - .blur(15.dp) - } else { - modifier.fillMaxSize() - }, - imageModel = { userImage }, - previewPlaceholder = painterResource(id = R.drawable.stream_video_call_sample), - imageOptions = ImageOptions( - contentScale = ContentScale.Crop, - contentDescription = null, - ), - component = rememberImageComponent { - +CrossfadePlugin() - +BlurTransformationPlugin(blurRadius) - }, - ) - } else { - DefaultCallBackground() - } -} - -@Composable -private fun DefaultCallBackground() { - val backgroundBrush = Brush.linearGradient( - listOf( - VideoTheme.colors.callGradientStart, - VideoTheme.colors.callGradientEnd, - ), - ) - Box( - modifier = Modifier + modifier = modifier .fillMaxSize() - .background(backgroundBrush), - ) + .background(color = VideoTheme.colors.baseSheetTertiary), + ) { + content() + } } @Preview @@ -170,11 +54,7 @@ private fun DefaultCallBackground() { private fun CallBackgroundPreview() { StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) VideoTheme { - CallBackground( - participants = previewMemberListState.take(1), - isVideoType = true, - isIncoming = true, - ) { + CallBackground { Box(modifier = Modifier.align(Alignment.Center)) { AvatarImagePreview() } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/Badge.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/Badge.kt new file mode 100644 index 0000000000..db8eb6e5ef --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/Badge.kt @@ -0,0 +1,129 @@ +/* + * 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.compose.ui.components.base + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidthIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddAlert +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.QuestionAnswer +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.styling.BadgeStyle +import io.getstream.video.android.compose.ui.components.base.styling.ButtonStyles +import io.getstream.video.android.compose.ui.components.base.styling.StreamBadgeStyles + +@Composable +public fun StreamBadgeBox( + modifier: Modifier = Modifier, + text: String? = null, + showWithoutValue: Boolean = true, + style: BadgeStyle = VideoTheme.styles.badgeStyles.defaultBadgeStyle(), + content: @Composable BoxScope.(Modifier) -> Unit, +) { + Box(modifier = modifier) { + content(modifier) + // Badge content + if (text != null || showWithoutValue) { + BadgeContent(style = style, text = text) + } + } +} + +@Composable +private fun BoxScope.BadgeContent(style: BadgeStyle, text: String? = null) { + Box( + modifier = Modifier + .height(style.size) + .requiredWidthIn(min = style.size) + .align(Alignment.TopEnd) + .background(style.color, CircleShape), + ) { + Text( + modifier = Modifier + .align(Alignment.Center) + .wrapContentSize() + .padding(horizontal = 4.dp), + text = text ?: "", + style = style.textStyle, + ) + } +} + +@Preview +@Composable +private fun ButtonWithBadgePreview() { + VideoTheme { + Column { + StreamBadgeBox( + text = "!", + style = StreamBadgeStyles.defaultBadgeStyle(), + ) { + StreamIconButton( + icon = Icons.Default.AddAlert, + style = ButtonStyles.secondaryIconButtonStyle(), + ) + } + Spacer(modifier = Modifier.size(16.dp)) + StreamBadgeBox( + text = "10", + style = StreamBadgeStyles.defaultBadgeStyle(), + ) { + StreamButton( + icon = Icons.Default.Info, + text = "Secondary Button", + style = ButtonStyles.secondaryButtonStyle(), + ) + } + Spacer(modifier = Modifier.size(16.dp)) + StreamBadgeBox( + text = "10+", + style = StreamBadgeStyles.defaultBadgeStyle(), + ) { + StreamIconButton( + icon = Icons.Default.QuestionAnswer, + style = ButtonStyles.primaryIconButtonStyle(), + ) + } + Spacer(modifier = Modifier.size(16.dp)) + StreamBadgeBox( + showWithoutValue = false, + style = StreamBadgeStyles.defaultBadgeStyle(), + ) { + StreamIconButton( + icon = Icons.Default.QuestionAnswer, + style = ButtonStyles.primaryIconButtonStyle(), + ) + } + } + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/Container.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/Container.kt new file mode 100644 index 0000000000..a50026c7aa --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/Container.kt @@ -0,0 +1,56 @@ +/* + * 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.compose.ui.components.base + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +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.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import io.getstream.video.android.compose.theme.base.VideoTheme + +@Composable +public fun GenericContainer( + modifier: Modifier = Modifier, + background: Color = VideoTheme.colors.buttonPrimaryDefault, + roundness: Dp = VideoTheme.dimens.roundnessL, + content: @Composable BoxScope.() -> Unit, +): Unit = Box( + modifier = modifier + .background( + color = background, + shape = RoundedCornerShape(roundness), + ) + .padding(VideoTheme.dimens.spacingXs), + content = content, +) + +@Preview +@Composable +private fun GenericContainerPreview() { + VideoTheme { + GenericContainer { + Text(text = "Contained text!", color = Color.White) + } + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/Dialogs.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/Dialogs.kt new file mode 100644 index 0000000000..a7bab84f24 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/Dialogs.kt @@ -0,0 +1,237 @@ +/* + * 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.compose.ui.components.base + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +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.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.StopCircle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +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 androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.styling.ButtonStyles +import io.getstream.video.android.compose.ui.components.base.styling.DialogStyle +import io.getstream.video.android.compose.ui.components.base.styling.StreamButtonStyle +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 + +@Composable +public fun StreamDialog( + modifier: Modifier = Modifier, + onDismiss: () -> Unit = {}, + style: DialogStyle, + dialogProperties: DialogProperties = DialogProperties(), + content: @Composable BoxScope.() -> Unit, +): Unit = Dialog(onDismissRequest = onDismiss, dialogProperties) { + Box( + modifier = modifier + .clip(style.shape) + .background(color = style.backgroundColor, shape = style.shape) + .padding(style.contentPaddings), + contentAlignment = Alignment.Center, + ) { + content() + } +} + +@Composable +public fun StreamDialogPositiveNegative( + modifier: Modifier = Modifier, + onDismiss: () -> Unit = {}, + dialogProperties: DialogProperties = DialogProperties(), + style: DialogStyle = VideoTheme.styles.dialogStyles.defaultDialogStyle(), + title: String? = null, + icon: ImageVector? = null, + contentText: String? = null, + positiveButton: Triple Unit>, + negativeButton: Triple Unit>? = null, + content: (@Composable () -> Unit)? = null, +): Unit = StreamDialog( + modifier = modifier, + style = style, + onDismiss = onDismiss, + dialogProperties = dialogProperties, +) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + icon?.let { + val iconStyle = style.iconStyle + Icon( + imageVector = icon, + contentDescription = "", + tint = iconStyle.color, + ) + Spacer(modifier = Modifier.width(iconStyle.padding.calculateTopPadding())) + } + title?.let { + Text(text = it, style = style.titleStyle) + } + } + Spacer(modifier = Modifier.size(8.dp)) + contentText?.let { + Text(text = it, style = style.contentTextStyle) + Spacer(modifier = Modifier.size(32.dp)) + } + content?.let { + content() + Spacer(modifier = Modifier.size(32.dp)) + } + val horizontalArr = if (negativeButton == null) { + Arrangement.End + } else { + Arrangement.SpaceBetween + } + Spacer(modifier = Modifier.size(8.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = horizontalArr) { + negativeButton?.let { + StreamButton( + modifier = Modifier.weight(1f), + text = it.first, + style = it.second, + onClick = it.third, + ) + Spacer(modifier = Modifier.size(16.dp)) + } + StreamButton( + modifier = Modifier.weight(1f), + text = positiveButton.first, + style = positiveButton.second, + onClick = positiveButton.third, + ) + } + } +} + +@Preview +@Composable +private fun StreamDialogPreview() { + VideoTheme { + StreamDialogPositiveNegative( + icon = Icons.Default.StopCircle, + title = "This Call is Being Recorded", + // Color is for preview only + style = StreamDialogStyles.defaultDialogStyle().copy( + backgroundColor = VideoTheme.colors.baseSheetTertiary, + ), + contentText = "By staying in the call you’re consenting to being recorded.", + positiveButton = Triple( + "Continue", + ButtonStyles.secondaryButtonStyle(StyleSize.S), + ) { + // Do nothing + }, + negativeButton = Triple( + "Leave call", + ButtonStyles.tetriaryButtonStyle(StyleSize.S), + ) { + // Do nothing + }, + ) + } +} + +@Preview +@Composable +private fun StreamDialogWithInputPreview() { + VideoTheme { + StreamDialogPositiveNegative( + content = { + 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 = TextFieldValue(""), + placeholder = "Email address (required)", + onValueChange = {}, + style = StreamTextFieldStyles.defaultTextField(StyleSize.S), + ) + + Spacer(modifier = Modifier.size(16.dp)) + StreamTextField( + value = TextFieldValue(""), + placeholder = "Message", + onValueChange = {}, + minLines = 7, + style = StreamTextFieldStyles.defaultTextField(StyleSize.S), + ) + }, + // Color is for preview only + style = StreamDialogStyles.defaultDialogStyle().copy( + backgroundColor = VideoTheme.colors.baseSheetTertiary, + ), + positiveButton = Triple( + "Submit", + ButtonStyles.secondaryButtonStyle(StyleSize.S), + ) { + // Do nothing + }, + negativeButton = Triple( + "Not now", + ButtonStyles.tetriaryButtonStyle(StyleSize.S), + ) { + // Do nothing + }, + ) + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/InputFields.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/InputFields.kt new file mode 100644 index 0000000000..946888fc21 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/InputFields.kt @@ -0,0 +1,225 @@ +/* + * 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.compose.ui.components.base + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Icon +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ErrorOutline +import androidx.compose.material.icons.outlined.Phone +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.styling.StreamTextFieldStyles +import io.getstream.video.android.compose.ui.components.base.styling.TextFieldStyle +import io.getstream.video.android.compose.ui.components.base.styling.styleState + +@Composable +public fun StreamOutlinedTextField( + modifier: Modifier = Modifier, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + enabled: Boolean = true, + readOnly: Boolean = false, + style: TextFieldStyle, + placeholder: @Composable (() -> Unit)? = null, + isError: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions(), + singleLine: Boolean = false, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + leadingIcon: (@Composable () -> Unit)? = null, + trailingIcon: (@Composable () -> Unit)? = null, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +): Unit = OutlinedTextField( + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + modifier = modifier, + enabled = enabled, + readOnly = readOnly, + placeholder = placeholder, + isError = isError, + visualTransformation = visualTransformation, + keyboardActions = keyboardActions, + keyboardOptions = keyboardOptions, + maxLines = maxLines, + minLines = minLines, + interactionSource = interactionSource, + shape = style.shape, + value = value, + colors = style.colors, + onValueChange = onValueChange, +) + +@Composable +public fun StreamTextField( + modifier: Modifier = Modifier, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + enabled: Boolean = true, + readOnly: Boolean = false, + style: TextFieldStyle = VideoTheme.styles.textFieldStyles.defaultTextField(), + placeholder: String? = null, + error: Boolean = false, + maxLines: Int = 1, + minLines: Int = 1, + icon: ImageVector? = null, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +): Unit = StreamOutlinedTextField( + modifier = modifier, + enabled = enabled, + readOnly = readOnly, + placeholder = @Composable { + val textStyle = style.placeholderStyle.of( + state = styleState( + interactionSource = interactionSource, + enabled = enabled, + ), + ) + Text( + text = placeholder ?: "", + style = textStyle.value.platform, + ) + }, + leadingIcon = icon?.let { + @Composable { + val iconStyle = style.iconStyle.of( + state = styleState( + interactionSource = interactionSource, + enabled = enabled, + ), + ) + Icon(imageVector = it, contentDescription = it.name, tint = iconStyle.value.color) + } + }, + trailingIcon = error.takeIf { it }?.let { + @Composable { + Icon( + tint = style.colors.trailingIconColor(enabled = enabled, isError = true).value, + imageVector = Icons.Default.ErrorOutline, + contentDescription = Icons.Default.ErrorOutline.name, + ) + } + }, + style = style, + isError = error, + visualTransformation = visualTransformation, + keyboardActions = keyboardActions, + keyboardOptions = keyboardOptions, + singleLine = maxLines == 1, + maxLines = Math.max(minLines, maxLines), + minLines = minLines, + interactionSource = interactionSource, + value = value, + onValueChange = onValueChange, +) + +@Preview +@Composable +private fun StreamInputFieldsPreviews() { + VideoTheme { + Column { + // Empty + StreamTextField( + value = TextFieldValue(""), + placeholder = "Call ID", + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamTextField( + value = TextFieldValue(""), + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + + StreamTextField( + icon = Icons.Outlined.Phone, + value = TextFieldValue(""), + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + + // Not empty + StreamTextField( + value = TextFieldValue("Some value"), + placeholder = "Call ID", + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + + StreamTextField( + icon = Icons.Outlined.Phone, + value = TextFieldValue("+ 123 456 789"), + placeholder = "Call ID", + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + + // Disabled + StreamTextField( + enabled = false, + value = TextFieldValue(""), + placeholder = "Call ID", + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + + // Error + StreamTextField( + error = true, + value = TextFieldValue("Wrong data"), + placeholder = "Call ID", + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + + StreamTextField( + value = TextFieldValue(""), + placeholder = "Message", + onValueChange = { }, + minLines = 8, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + } + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/StreamButton.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/StreamButton.kt new file mode 100644 index 0000000000..8d21011c7f --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/StreamButton.kt @@ -0,0 +1,572 @@ +/* + * 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.compose.ui.components.base + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccessAlarm +import androidx.compose.material.icons.filled.Camera +import androidx.compose.material.icons.filled.ExitToApp +import androidx.compose.material.icons.filled.GridView +import androidx.compose.material.icons.filled.GroupAdd +import androidx.compose.material.icons.filled.PhoneMissed +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material.icons.filled.VideocamOff +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.styling.ButtonStyles +import io.getstream.video.android.compose.ui.components.base.styling.StreamBadgeStyles +import io.getstream.video.android.compose.ui.components.base.styling.StreamButtonStyle +import io.getstream.video.android.compose.ui.components.base.styling.StreamFixedSizeButtonStyle +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize +import io.getstream.video.android.compose.ui.components.base.styling.styleState + +@Composable +public fun GenericStreamButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + enabled: Boolean = true, + style: StreamButtonStyle = ButtonStyles.primaryButtonStyle(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + content: @Composable RowScope.() -> Unit, +): Unit = Button( + interactionSource = interactionSource, + enabled = enabled, + modifier = modifier, + elevation = style.elevation, + shape = style.shape, + colors = style.colors, + border = style.border, + contentPadding = style.contentPadding, + onClick = onClick, + content = content, +) + +@Composable +public fun StreamButton( + modifier: Modifier = Modifier, + icon: ImageVector? = null, + text: String, + enabled: Boolean = true, + showProgress: Boolean = false, + style: StreamButtonStyle = VideoTheme.styles.buttonStyles.primaryButtonStyle(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + onClick: () -> Unit = { }, +): Unit = GenericStreamButton( + modifier = modifier, + style = style, + onClick = onClick, + interactionSource = interactionSource, + enabled = enabled, +) { + val state = styleState(interactionSource, enabled) + icon?.let { + val iconStyle = style.iconStyle.of(state = state).value + Icon( + imageVector = icon, + contentDescription = "", + tint = iconStyle.color, + ) + Spacer(modifier = Modifier.width(iconStyle.padding.calculateTopPadding())) + } + val textStyle = style.textStyle.of(state = state) + Text( + style = textStyle.value.platform, + text = text, + ) + if (showProgress) { + CircularProgressIndicator( + color = textStyle.value.platform.color, + modifier = Modifier.height(VideoTheme.dimens.genericS), + ) + } +} + +@Composable +public fun StreamIconButton( + modifier: Modifier = Modifier, + icon: ImageVector, + style: StreamFixedSizeButtonStyle, + onClick: () -> Unit = {}, + enabled: Boolean = true, + showProgress: Boolean = false, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + val state = styleState(interactionSource = interactionSource, enabled = enabled) + val iconStyle = style.iconStyle.of(state = state).value + + Button( + interactionSource = interactionSource, + enabled = enabled, + modifier = modifier + .requiredWidth(style.width) + .requiredHeight(style.height) + .aspectRatio(style.height / style.width), + elevation = style.elevation, + shape = style.shape, + colors = style.colors, + border = style.border, + contentPadding = iconStyle.padding, + onClick = onClick, + ) { + if (showProgress) { + CircularProgressIndicator( + color = iconStyle.color, + ) + } else { + Icon( + tint = iconStyle.color, + imageVector = icon, + contentDescription = icon.name, + ) + } + } +} + +@Composable +public fun GenericToggleButton( + modifier: Modifier = Modifier, + toggleState: State = rememberUpdatedState(newValue = ToggleableState(false)), + onClick: (ToggleableState) -> Unit = {}, + onContent: @Composable BoxScope.(onClick: (ToggleableState) -> Unit) -> Unit, + offContent: @Composable BoxScope.(onClick: (ToggleableState) -> Unit) -> Unit, +): Unit = Box(modifier = modifier) { + if (toggleState.value == ToggleableState.On) { + onContent(onClick) + } else { + offContent(onClick) + } +} + +@Composable +public fun StreamIconToggleButton( + modifier: Modifier = Modifier, + toggleState: State = rememberUpdatedState(newValue = ToggleableState(false)), + onIcon: ImageVector, + offIcon: ImageVector = onIcon, + onStyle: StreamFixedSizeButtonStyle, + showProgress: Boolean = false, + offStyle: StreamFixedSizeButtonStyle = onStyle, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + onClick: (ToggleableState) -> Unit = {}, +): Unit = GenericToggleButton( + modifier = modifier, + toggleState = toggleState, + onClick = onClick, + onContent = { + StreamIconButton( + showProgress = showProgress, + interactionSource = interactionSource, + enabled = enabled, + icon = onIcon, + style = onStyle, + onClick = { + it(toggleState.value) + }, + ) + }, + offContent = { + StreamIconButton( + showProgress = showProgress, + interactionSource = interactionSource, + enabled = enabled, + icon = offIcon, + style = offStyle, + onClick = { + it(toggleState.value) + }, + ) + }, +) + +@Composable +public fun StreamToggleButton( + modifier: Modifier = Modifier, + toggleState: State = rememberUpdatedState(newValue = ToggleableState(false)), + onText: String, + offText: String, + onIcon: ImageVector? = null, + offIcon: ImageVector? = onIcon, + onStyle: StreamButtonStyle, + offStyle: StreamButtonStyle = onStyle, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + onClick: (ToggleableState) -> Unit = {}, +): Unit = GenericToggleButton( + modifier = modifier, + toggleState = toggleState, + onClick = onClick, + onContent = { + GenericStreamButton( + modifier = Modifier.align(Alignment.CenterStart), + interactionSource = interactionSource, + enabled = enabled, + style = onStyle, + onClick = { + it(toggleState.value) + }, + ) { + val state = styleState(interactionSource, enabled) + onIcon?.let { + val iconStyle = onStyle.iconStyle.of(state = state).value + Icon( + imageVector = it, + contentDescription = "", + tint = iconStyle.color, + ) + Spacer(modifier = Modifier.width(iconStyle.padding.calculateTopPadding())) + } + val textStyle = onStyle.textStyle.of(state = state) + Text( + modifier = Modifier.fillMaxWidth(), + style = textStyle.value.platform, + text = onText, + ) + } + }, + offContent = { + GenericStreamButton( + modifier = Modifier.align(Alignment.CenterStart), + interactionSource = interactionSource, + enabled = enabled, + style = offStyle, + onClick = { + it(toggleState.value) + }, + ) { + val state = styleState(interactionSource, enabled) + offIcon?.let { + val iconStyle = offStyle.iconStyle.of(state = state).value + Icon( + imageVector = it, + contentDescription = "", + tint = iconStyle.color, + ) + Spacer(modifier = Modifier.width(iconStyle.padding.calculateTopPadding())) + } + val textStyle = offStyle.textStyle.of(state = state) + Text( + modifier = Modifier.fillMaxWidth(), + style = textStyle.value.platform, + text = offText, + ) + } + }, +) + +// Start of previews +@Preview +@Composable +private fun StreamIconButtonPreview() { + VideoTheme { + Column { + Row { + StreamIconButton( + icon = Icons.Default.GroupAdd, + style = ButtonStyles.primaryIconButtonStyle(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + icon = Icons.Default.ExitToApp, + style = ButtonStyles.secondaryIconButtonStyle(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + icon = Icons.Default.Settings, + style = ButtonStyles.tetriaryIconButtonStyle(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + icon = Icons.Default.PhoneMissed, + style = ButtonStyles.alertIconButtonStyle(), + ) + } + + Spacer(modifier = Modifier.size(48.dp)) + Row { + StreamIconButton( + enabled = false, + icon = Icons.Default.GroupAdd, + style = ButtonStyles.primaryIconButtonStyle(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + enabled = false, + icon = Icons.Default.ExitToApp, + style = ButtonStyles.secondaryIconButtonStyle(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + enabled = false, + icon = Icons.Default.Settings, + style = ButtonStyles.tetriaryIconButtonStyle(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + enabled = false, + icon = Icons.Default.PhoneMissed, + style = ButtonStyles.alertIconButtonStyle(), + ) + } + + Spacer(modifier = Modifier.size(48.dp)) + Row { + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + icon = Icons.Default.PhoneMissed, + style = ButtonStyles.alertIconButtonStyle(size = StyleSize.L), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + icon = Icons.Default.PhoneMissed, + style = ButtonStyles.alertIconButtonStyle(size = StyleSize.M), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + icon = Icons.Default.PhoneMissed, + style = ButtonStyles.alertIconButtonStyle(size = StyleSize.S), + ) + } + } + } +} + +@Preview +@Composable +private fun StreamButtonPreview() { + VideoTheme { + Column { + // Default + StreamButton( + text = "Primary Button", + style = ButtonStyles.primaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + text = "Secondary Button", + style = ButtonStyles.secondaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + text = "Tetriary Button", + style = ButtonStyles.tetriaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(48.dp)) + StreamButton( + text = "Alert Button", + style = ButtonStyles.alertButtonStyle(), + ) + Spacer(modifier = Modifier.height(48.dp)) + + // Disabled + StreamButton( + enabled = false, + text = "Primary Button", + style = ButtonStyles.primaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + enabled = false, + text = "Secondary Button", + style = ButtonStyles.secondaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + enabled = false, + text = "Tetriary Button", + style = ButtonStyles.tetriaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + enabled = false, + text = "Alert Button", + style = ButtonStyles.alertButtonStyle(), + ) + Spacer(modifier = Modifier.height(48.dp)) + } + } +} + +@Preview +@Composable +private fun StreamButtonWithIconPreview() { + VideoTheme { + Column { + // With icon + StreamButton( + icon = Icons.Filled.AccessAlarm, + text = "Primary Button", + style = ButtonStyles.primaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + icon = Icons.Filled.AccessAlarm, + text = "Secondary Button", + style = ButtonStyles.secondaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + icon = Icons.Filled.AccessAlarm, + text = "Tetriary Button", + style = ButtonStyles.tetriaryButtonStyle(), + ) + } + } +} + +@Preview +@Composable +private fun StreamButtonSizePreview() { + VideoTheme { + Column { + // Size + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + text = "Small Button", + style = ButtonStyles.secondaryButtonStyle(size = StyleSize.S), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + text = "Medium Button", + style = ButtonStyles.secondaryButtonStyle(size = StyleSize.M), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + text = "Large Button", + style = ButtonStyles.secondaryButtonStyle(size = StyleSize.L), + ) + } + } +} + +@Preview +@Composable +private fun StreamToggleIconButtonPreview() { + VideoTheme { + Row { + // Size + StreamIconToggleButton( + onStyle = ButtonStyles.primaryIconButtonStyle(), + offStyle = ButtonStyles.alertIconButtonStyle(), + onIcon = Icons.Default.Videocam, + offIcon = Icons.Default.VideocamOff, + ) + Spacer(modifier = Modifier.width(24.dp)) + StreamIconToggleButton( + toggleState = rememberUpdatedState(newValue = ToggleableState.On), + onStyle = ButtonStyles.primaryIconButtonStyle(), + offStyle = ButtonStyles.alertIconButtonStyle(), + onIcon = Icons.Default.Videocam, + offIcon = Icons.Default.VideocamOff, + ) + + Spacer(modifier = Modifier.width(24.dp)) + StreamBadgeBox( + style = StreamBadgeStyles.defaultBadgeStyle().copy( + color = VideoTheme.colors.alertCaution, + textStyle = VideoTheme.typography.labelXS.copy(color = Color.Black), + ), + text = "!", + ) { + StreamIconToggleButton( + enabled = false, + toggleState = rememberUpdatedState(newValue = ToggleableState.Off), + onStyle = ButtonStyles.secondaryIconButtonStyle(), + offStyle = ButtonStyles.alertIconButtonStyle(), + onIcon = Icons.Default.Videocam, + offIcon = Icons.Default.VideocamOff, + ) + } + } + } +} + +@Preview +@Composable +private fun StreamToggleButtonPreview() { + VideoTheme { + Column { + // Size + StreamToggleButton( + modifier = Modifier.fillMaxWidth(), + toggleState = rememberUpdatedState(newValue = ToggleableState.On), + onText = "Grid", + offText = "Grid", + onStyle = ButtonStyles.toggleButtonStyleOn(), + offStyle = ButtonStyles.toggleButtonStyleOff(), + onIcon = Icons.Filled.GridView, + offIcon = Icons.Filled.GridView, + ) + Spacer(modifier = Modifier.width(24.dp)) + StreamToggleButton( + modifier = Modifier.fillMaxWidth(), + toggleState = rememberUpdatedState(newValue = ToggleableState.Off), + onText = "Grid (On)", + offText = "Grid (Off)", + onStyle = ButtonStyles.toggleButtonStyleOn(), + offStyle = ButtonStyles.toggleButtonStyleOff(), + onIcon = Icons.Filled.GridView, + offIcon = Icons.Filled.GridView, + ) + } + } +} + +@Preview +@Composable +private fun StreamProgressButtonsPreview() { + VideoTheme { + Column { + StreamButton(text = "Progress", showProgress = true) + StreamIconButton( + icon = Icons.Default.Camera, + style = VideoTheme.styles.buttonStyles.secondaryIconButtonStyle(), + showProgress = true, + ) + } + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/Badge.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/Badge.kt new file mode 100644 index 0000000000..d6e471c400 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/Badge.kt @@ -0,0 +1,51 @@ +/* + * 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.compose.ui.components.base.styling + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.getstream.video.android.compose.theme.base.VideoTheme + +public data class BadgeStyle( + public val size: Dp, + public val color: Color, + public val textStyle: TextStyle, + public val contentPaddings: PaddingValues, +) : StreamStyle + +public open class BadgeStyleProvider { + @Composable + public fun defaultBadgeStyle(): BadgeStyle = BadgeStyle( + color = VideoTheme.colors.alertSuccess, + size = 16.dp, + textStyle = TextStyle( + fontSize = 8.sp, + lineHeight = 10.sp, + fontWeight = FontWeight.W600, + color = VideoTheme.colors.baseTertiary, + ), + contentPaddings = PaddingValues(VideoTheme.dimens.genericXs, 0.dp), + ) +} + +public object StreamBadgeStyles : BadgeStyleProvider() diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/Colors.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/CompositeStyleProvider.kt similarity index 51% rename from demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/Colors.kt rename to stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/CompositeStyleProvider.kt index f8b262b3c4..a798c3e3de 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/theme/Colors.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/CompositeStyleProvider.kt @@ -14,12 +14,16 @@ * limitations under the License. */ -package io.getstream.video.android.ui.theme +package io.getstream.video.android.compose.ui.components.base.styling -import androidx.compose.ui.graphics.Color - -object Colors { - val background: Color = Color(0xFF2C2C2E) - val secondBackground: Color = Color(0xFF1C1E22) - val description: Color = Color(0xFF979797) -} +/** + * Composite styles provider providing various components styles. + */ +public open class CompositeStyleProvider( + public val iconStyles: IconStyleProvider = IconStyles, + public val textFieldStyles: TextFieldStyleProvider = StreamTextFieldStyles, + public val textStyles: TextStyleProvider = StreamTextStyles, + public val buttonStyles: ButtonStyleProvider = ButtonStyles, + public val dialogStyles: DialogStyleProvider = StreamDialogStyles, + public val badgeStyles: BadgeStyleProvider = StreamBadgeStyles, +) diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/DialogStyle.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/DialogStyle.kt new file mode 100644 index 0000000000..7a7425ce5d --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/DialogStyle.kt @@ -0,0 +1,47 @@ +/* + * 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.compose.ui.components.base.styling + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import io.getstream.video.android.compose.theme.base.VideoTheme + +public data class DialogStyle( + public val shape: Shape, + public val backgroundColor: Color, + public val titleStyle: TextStyle, + public val contentTextStyle: TextStyle, + public val iconStyle: IconStyle, + public val contentPaddings: PaddingValues, +) : StreamStyle + +public open class DialogStyleProvider { + @Composable + public fun defaultDialogStyle(): DialogStyle = DialogStyle( + shape = VideoTheme.shapes.dialog, + backgroundColor = VideoTheme.colors.baseSheetSecondary, + titleStyle = StreamTextStyles.defaultTitle(StyleSize.S).default.platform, + contentTextStyle = StreamTextStyles.defaultBody(StyleSize.S).default.platform, + iconStyle = IconStyles.defaultIconStyle().default, + contentPaddings = PaddingValues(VideoTheme.dimens.spacingL), + ) +} + +public object StreamDialogStyles : DialogStyleProvider() diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle.kt new file mode 100644 index 0000000000..b2735c4227 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/StreamButtonStyle.kt @@ -0,0 +1,325 @@ +/* + * 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.compose.ui.components.base.styling + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material.ButtonColors +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.ButtonElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.getstream.video.android.compose.theme.base.VideoTheme + +public object ButtonStyles : ButtonStyleProvider() + +@Immutable +public open class StreamButtonStyle( + public val elevation: ButtonElevation?, + public val shape: Shape, + public val border: BorderStroke?, + public val colors: ButtonColors, + public val contentPadding: PaddingValues, + public val textStyle: StreamTextStyle, + public val iconStyle: StreamIconStyle, +) { + + /** + * Standard copy function, to utilize as much as possible from the companion object. + */ + public fun copy( + elevation: ButtonElevation? = this.elevation, + shape: Shape = this.shape, + border: BorderStroke? = this.border, + colors: ButtonColors = this.colors, + contentPadding: PaddingValues = this.contentPadding, + textStyle: StreamTextStyle = this.textStyle, + iconStyle: StreamIconStyle = this.iconStyle, + ): StreamButtonStyle { + return StreamButtonStyle( + elevation, + shape, + border, + colors, + contentPadding, + textStyle, + iconStyle, + ) + } +} + +/** + * A style that also suggest a fixed size. Size is not guaranteed and depends on the implementing composable. + */ +public open class StreamFixedSizeButtonStyle( + public val width: Dp, + public val height: Dp, + elevation: ButtonElevation?, + shape: Shape, + border: BorderStroke?, + colors: ButtonColors, + contentPadding: PaddingValues, + textStyle: StreamTextStyle, + iconStyle: StreamIconStyle, +) : StreamButtonStyle(elevation, shape, border, colors, contentPadding, textStyle, iconStyle) { + + public fun copyFixed( + width: Dp = this.width, + height: Dp = this.height, + elevation: ButtonElevation? = this.elevation, + shape: Shape = this.shape, + border: BorderStroke? = this.border, + colors: ButtonColors = this.colors, + contentPadding: PaddingValues = this.contentPadding, + textStyle: StreamTextStyle = this.textStyle, + iconStyle: StreamIconStyle = this.iconStyle, + ): StreamFixedSizeButtonStyle = StreamFixedSizeButtonStyle( + width, + height, + elevation, + shape, + border, + colors, + contentPadding, + textStyle, + iconStyle, + ) + public companion object { + public fun of( + width: Dp, + height: Dp, + origin: StreamButtonStyle, + ): StreamFixedSizeButtonStyle = StreamFixedSizeButtonStyle( + width, + height, + origin.elevation, + origin.shape, + origin.border, + origin.colors, + origin.contentPadding, + origin.textStyle, + origin.iconStyle, + ) + } +} + +@Immutable +public open class ButtonStyleProvider { + + /** + * You can create any style with the [StreamButtonStyle]. + * Use it with the [StreamButton] composable + * + * @param elevation button elevation + * @param shape the shape of the button + * @param border the button border + * @param + */ + @Composable + public fun genericButtonStyle( + size: StyleSize = StyleSize.L, + elevation: ButtonElevation? = null, + shape: Shape = VideoTheme.shapes.button, + border: BorderStroke? = null, + colors: ButtonColors = ButtonDefaults.buttonColors(), + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + ): StreamButtonStyle = StreamButtonStyle( + elevation, + shape, + border, + colors, + contentPadding, + StreamTextStyles.defaultButtonLabel(size), + IconStyles.defaultIconStyle(), + ) + + @Composable + public fun primaryButtonStyle(size: StyleSize = StyleSize.L): StreamButtonStyle = + genericButtonStyle( + size = size, + shape = VideoTheme.shapes.button, + colors = ButtonDefaults.buttonColors( + backgroundColor = VideoTheme.colors.buttonPrimaryDefault, + contentColor = VideoTheme.colors.basePrimary, + disabledBackgroundColor = VideoTheme.colors.buttonPrimaryDisabled, + ), + contentPadding = PaddingValues( + start = VideoTheme.dimens.componentPaddingStart, + end = VideoTheme.dimens.componentPaddingEnd, + top = VideoTheme.dimens.componentPaddingTop, + bottom = VideoTheme.dimens.componentPaddingBottom, + ), + ) + + @Composable + public fun secondaryButtonStyle(size: StyleSize = StyleSize.L): StreamButtonStyle = + genericButtonStyle(size = size).copy( + colors = ButtonDefaults.buttonColors( + backgroundColor = VideoTheme.colors.buttonBrandDefault, + contentColor = VideoTheme.colors.basePrimary, + disabledBackgroundColor = VideoTheme.colors.buttonBrandDisabled, + ), + ) + + @Composable + public fun tetriaryButtonStyle(size: StyleSize = StyleSize.L): StreamButtonStyle = + genericButtonStyle(size = size).copy( + colors = ButtonDefaults.buttonColors( + backgroundColor = VideoTheme.colors.baseSheetPrimary, + contentColor = VideoTheme.colors.basePrimary, + disabledBackgroundColor = VideoTheme.colors.baseSheetPrimary, + ), + border = BorderStroke(1.dp, VideoTheme.colors.baseSenary), + ) + + @Composable + public fun toggleButtonStyleOn(size: StyleSize = StyleSize.L): StreamButtonStyle = + genericButtonStyle(size = size).copy( + colors = ButtonDefaults.buttonColors( + backgroundColor = VideoTheme.colors.buttonPrimaryDefault, + contentColor = VideoTheme.colors.basePrimary, + disabledBackgroundColor = VideoTheme.colors.buttonPrimaryDisabled, + ), + iconStyle = IconStyles.customColorIconStyle( + color = VideoTheme.colors.brandPrimary, + ), + ) + + @Composable + public fun toggleButtonStyleOff(size: StyleSize = StyleSize.L): StreamButtonStyle = + genericButtonStyle(size = size).copy( + colors = ButtonDefaults.buttonColors( + backgroundColor = VideoTheme.colors.baseSheetPrimary, + contentColor = VideoTheme.colors.basePrimary, + disabledBackgroundColor = VideoTheme.colors.baseSheetPrimary, + ), + ) + + @Composable + public fun alertButtonStyle(size: StyleSize = StyleSize.L): StreamButtonStyle = + genericButtonStyle(size = size).copy( + colors = ButtonDefaults.buttonColors( + backgroundColor = VideoTheme.colors.buttonAlertDefault, + contentColor = VideoTheme.colors.basePrimary, + disabledBackgroundColor = VideoTheme.colors.buttonAlertDisabled, + ), + ) + + @Composable + public fun primaryIconButtonStyle(size: StyleSize = StyleSize.L): StreamFixedSizeButtonStyle = + StreamFixedSizeButtonStyle.of( + width = when (size) { + StyleSize.XS, StyleSize.S -> VideoTheme.dimens.componentHeightS + StyleSize.M -> VideoTheme.dimens.componentHeightM + else -> VideoTheme.dimens.componentHeightL + }, + height = when (size) { + StyleSize.XS, StyleSize.S -> VideoTheme.dimens.componentHeightS + StyleSize.M -> VideoTheme.dimens.componentHeightM + else -> VideoTheme.dimens.componentHeightL + }, + origin = primaryButtonStyle(size = size).copy( + iconStyle = IconStyles.defaultIconStyle( + padding = PaddingValues(VideoTheme.dimens.spacingXs), + ), + ), + ) + + @Composable + public fun secondaryIconButtonStyle(size: StyleSize = StyleSize.L): StreamFixedSizeButtonStyle = + StreamFixedSizeButtonStyle.of( + width = when (size) { + StyleSize.XS, StyleSize.S -> VideoTheme.dimens.componentHeightS + StyleSize.M -> VideoTheme.dimens.componentHeightM + else -> VideoTheme.dimens.componentHeightL + }, + height = when (size) { + StyleSize.XS, StyleSize.S -> VideoTheme.dimens.componentHeightS + StyleSize.M -> VideoTheme.dimens.componentHeightM + else -> VideoTheme.dimens.componentHeightL + }, + origin = secondaryButtonStyle(size = size).copy( + iconStyle = IconStyles.defaultIconStyle( + padding = PaddingValues(VideoTheme.dimens.spacingXs), + ), + ), + ) + + @Composable + public fun tetriaryIconButtonStyle(size: StyleSize = StyleSize.L): StreamFixedSizeButtonStyle = + StreamFixedSizeButtonStyle.of( + width = when (size) { + StyleSize.XS, StyleSize.S -> VideoTheme.dimens.componentHeightS + StyleSize.M -> VideoTheme.dimens.componentHeightM + else -> VideoTheme.dimens.componentHeightL + }, + height = when (size) { + StyleSize.XS, StyleSize.S -> VideoTheme.dimens.componentHeightS + StyleSize.M -> VideoTheme.dimens.componentHeightM + else -> VideoTheme.dimens.componentHeightL + }, + origin = tetriaryButtonStyle(size = size).copy( + iconStyle = IconStyles.defaultIconStyle( + padding = PaddingValues(VideoTheme.dimens.spacingXs), + ), + ), + ) + + @Composable + public fun onlyIconIconButtonStyle(size: StyleSize = StyleSize.L): StreamFixedSizeButtonStyle = + StreamFixedSizeButtonStyle.of( + width = when (size) { + StyleSize.XS, StyleSize.S -> VideoTheme.dimens.componentHeightS + StyleSize.M -> VideoTheme.dimens.componentHeightM + else -> VideoTheme.dimens.componentHeightL + }, + height = when (size) { + StyleSize.XS, StyleSize.S -> VideoTheme.dimens.componentHeightS + StyleSize.M -> VideoTheme.dimens.componentHeightM + else -> VideoTheme.dimens.componentHeightL + }, + origin = tetriaryButtonStyle(size = size).copy( + iconStyle = IconStyles.defaultIconStyle( + padding = PaddingValues(VideoTheme.dimens.spacingXs), + ), + border = null, + ), + ) + + @Composable + public fun alertIconButtonStyle(size: StyleSize = StyleSize.L): StreamFixedSizeButtonStyle = + StreamFixedSizeButtonStyle.of( + width = when (size) { + StyleSize.XS, StyleSize.S -> VideoTheme.dimens.componentHeightS + StyleSize.M -> VideoTheme.dimens.componentHeightM + else -> VideoTheme.dimens.componentHeightL + }, + height = when (size) { + StyleSize.XS, StyleSize.S -> VideoTheme.dimens.componentHeightS + StyleSize.M -> VideoTheme.dimens.componentHeightM + else -> VideoTheme.dimens.componentHeightL + }, + origin = alertButtonStyle(size = size).copy( + iconStyle = IconStyles.defaultIconStyle( + padding = PaddingValues(VideoTheme.dimens.spacingXs), + ), + ), + ) +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/StreamIconsStyle.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/StreamIconsStyle.kt new file mode 100644 index 0000000000..3a23a5e69c --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/StreamIconsStyle.kt @@ -0,0 +1,83 @@ +/* + * 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.compose.ui.components.base.styling + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import io.getstream.video.android.compose.theme.base.VideoTheme + +/** + * Represents an icon style. Consists of color and padding. + * + * @param color the color applied as tint to the icon. + * @param padding the padding applied to the icon. + */ +public data class IconStyle( + val color: Color, + val padding: PaddingValues = PaddingValues(0.dp), +) : StreamStyle + +/** + * Contains state styles for the icons. + */ +public data class StreamIconStyle( + override val default: IconStyle, + override val pressed: IconStyle, + override val disabled: IconStyle, +) : StreamStateStyle + +/** + * Provides default icon style. + */ +public open class IconStyleProvider { + + /** + * Composable that provides default icon style. + * + * @param padding the padding values of the icon. + * @param default normal color of the icon. + * @param pressed color of the icon when pressed + * @param disabled color of the icon when disabled + * + * @see [StreamIconStyle] + */ + @Composable + public fun defaultIconStyle( + padding: PaddingValues = PaddingValues(VideoTheme.dimens.spacingM), + default: Color = VideoTheme.colors.basePrimary, + pressed: Color = VideoTheme.colors.basePrimary, + disabled: Color = VideoTheme.colors.baseQuaternary, + ): StreamIconStyle = StreamIconStyle( + default = IconStyle(default, padding), + pressed = IconStyle(pressed, padding), + disabled = IconStyle(disabled, padding), + ) + + @Composable + public fun customColorIconStyle(color: Color): StreamIconStyle = defaultIconStyle( + default = color, + pressed = color, + disabled = color.copy(alpha = 0.16f), + ) +} + +/** + * Object accessor for a default [IconStyleProvider] + */ +public object IconStyles : IconStyleProvider() diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/StreamStyle.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/StreamStyle.kt new file mode 100644 index 0000000000..0919650675 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/StreamStyle.kt @@ -0,0 +1,70 @@ +/* + * 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.compose.ui.components.base.styling + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.rememberUpdatedState + +/** + * Marker interface for all stream styles. + */ +@Stable +public interface StreamStyle + +/** + * Possible interaction states. + */ +public enum class StyleState { + ENABLED, DISABLED, PRESSED +} + +/** + * Possible sizes for the stile. + */ +public enum class StyleSize { + XS, S, M, L, XL, XXL +} + +/** + * Stream style container, containing multiple styles + */ +@Stable +public interface StreamStateStyle { + + /** Default style for the component. */ + public val default: T + + /** Pressed style for the component */ + public val pressed: T + + /** Disabled style for the component */ + public val disabled: T + + /** + * Get the style based on [StyleState]. + */ + @Composable + public fun of(state: StyleState): State = rememberUpdatedState( + when (state) { + StyleState.ENABLED -> default + StyleState.DISABLED -> disabled + StyleState.PRESSED -> pressed + }, + ) +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/StreamTextFieldStyles.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/StreamTextFieldStyles.kt new file mode 100644 index 0000000000..ba7805a6a6 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/StreamTextFieldStyles.kt @@ -0,0 +1,77 @@ +/* + * 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.compose.ui.components.base.styling + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material.TextFieldColors +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp +import io.getstream.video.android.compose.theme.base.VideoTheme + +public data class TextFieldStyle( + val textStyle: StreamTextStyle, + val placeholderStyle: StreamTextStyle, + val iconStyle: StreamIconStyle, + val colors: TextFieldColors, + val shape: Shape, + val borderStroke: BorderStroke?, + val paddings: PaddingValues, +) : StreamStyle + +public open class TextFieldStyleProvider { + + @Composable + public fun defaultTextField( + styleSize: StyleSize = StyleSize.S, + textStyle: StreamTextStyle = StreamTextStyles.defaultTextField(styleSize), + placeholderStyle: StreamTextStyle = StreamTextStyles.defaultSubtitle(styleSize), + iconStyle: StreamIconStyle = IconStyles.defaultIconStyle(), + ): TextFieldStyle = + TextFieldStyle( + textStyle = textStyle, + colors = TextFieldDefaults.outlinedTextFieldColors( + // Background + backgroundColor = VideoTheme.colors.baseSheetPrimary, + // Border + focusedBorderColor = VideoTheme.colors.brandPrimary, + unfocusedBorderColor = VideoTheme.colors.baseSenary, + disabledBorderColor = VideoTheme.colors.baseSenary.copy(alpha = 0.16f), + errorBorderColor = VideoTheme.colors.alertWarning, + // Cursor + cursorColor = VideoTheme.colors.basePrimary, + errorCursorColor = VideoTheme.colors.alertWarning, + // Text + textColor = VideoTheme.colors.basePrimary, + disabledTextColor = VideoTheme.colors.baseTertiary.copy(alpha = 0.16f), + errorLabelColor = VideoTheme.colors.alertWarning, + focusedLabelColor = VideoTheme.colors.basePrimary, + unfocusedLabelColor = VideoTheme.colors.basePrimary, + placeholderColor = VideoTheme.colors.baseTertiary, + disabledPlaceholderColor = VideoTheme.colors.baseTertiary.copy(alpha = 0.16f), + ), + borderStroke = BorderStroke(2.dp, VideoTheme.colors.baseSenary), + shape = VideoTheme.shapes.input, + placeholderStyle = placeholderStyle, + paddings = PaddingValues(VideoTheme.dimens.componentPaddingFixed), + iconStyle = iconStyle, + ) +} + +public object StreamTextFieldStyles : TextFieldStyleProvider() diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/TextStyles.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/TextStyles.kt new file mode 100644 index 0000000000..1243e3be3b --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/TextStyles.kt @@ -0,0 +1,141 @@ +/* + * 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.compose.ui.components.base.styling + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import io.getstream.video.android.compose.theme.base.VideoTheme + +/** + * Wrapper for the platform text style. + */ +public data class TextStyleWrapper( + public val platform: TextStyle, +) : StreamStyle + +/** + * Stream text style + */ +public data class StreamTextStyle( + override val default: TextStyleWrapper, + override val disabled: TextStyleWrapper, + override val pressed: TextStyleWrapper, +) : StreamStateStyle + +public open class TextStyleProvider { + + @Composable + public fun defaultLabel( + size: StyleSize = StyleSize.L, + default: TextStyleWrapper = when (size) { + StyleSize.XS, StyleSize.S -> VideoTheme.typography.labelS.wrapper() + StyleSize.M -> VideoTheme.typography.labelM.wrapper() + else -> VideoTheme.typography.labelL.wrapper() + }, + pressed: TextStyleWrapper = default, + disabled: TextStyleWrapper = default.disabledAlpha(), + ): StreamTextStyle = StreamTextStyle(default, disabled, pressed) + + @Composable + public fun defaultButtonLabel( + size: StyleSize = StyleSize.L, + default: TextStyleWrapper = when (size) { + StyleSize.XS, StyleSize.S -> VideoTheme.typography.labelXS.wrapper() + StyleSize.M -> VideoTheme.typography.labelS.wrapper() + else -> VideoTheme.typography.labelM.wrapper() + }, + pressed: TextStyleWrapper = default, + disabled: TextStyleWrapper = default.disabledAlpha(), + ): StreamTextStyle = StreamTextStyle(default, disabled, pressed) + + @Composable + public fun defaultTitle( + size: StyleSize = StyleSize.L, + default: TextStyleWrapper = when (size) { + StyleSize.XS -> VideoTheme.typography.titleXs.wrapper() + StyleSize.S -> VideoTheme.typography.titleS.wrapper() + StyleSize.M -> VideoTheme.typography.titleM.wrapper() + else -> VideoTheme.typography.titleL.wrapper() + }, + pressed: TextStyleWrapper = default, + disabled: TextStyleWrapper = default.disabledAlpha(), + ): StreamTextStyle = StreamTextStyle(default, disabled, pressed) + + @Composable + public fun defaultSubtitle( + size: StyleSize = StyleSize.M, + default: TextStyleWrapper = when (size) { + StyleSize.XS -> VideoTheme.typography.subtitleS.wrapper() + StyleSize.S -> VideoTheme.typography.subtitleS.wrapper() + StyleSize.M -> VideoTheme.typography.subtitleM.wrapper() + else -> VideoTheme.typography.subtitleL.wrapper() + }, + pressed: TextStyleWrapper = default, + disabled: TextStyleWrapper = default.disabledAlpha(), + ): StreamTextStyle = StreamTextStyle(default, disabled, pressed) + + @Composable + public fun defaultBody( + size: StyleSize = StyleSize.L, + default: TextStyleWrapper = when (size) { + StyleSize.XS, StyleSize.S, StyleSize.M -> VideoTheme.typography.bodyM.wrapper() + else -> VideoTheme.typography.bodyL.wrapper() + }, + pressed: TextStyleWrapper = default, + disabled: TextStyleWrapper = default.disabledAlpha(), + ): StreamTextStyle = StreamTextStyle(default, disabled, pressed) + + @Composable + public fun defaultBadgeTextStyle( + default: TextStyleWrapper = VideoTheme.typography.labelXS.wrapper(), + pressed: TextStyleWrapper = default, + disabled: TextStyleWrapper = default.disabledAlpha(), + ): StreamTextStyle = StreamTextStyle(default, disabled, pressed) + + @Composable + public fun defaultTextField( + size: StyleSize = StyleSize.M, + default: TextStyleWrapper = when (size) { + StyleSize.XS -> VideoTheme.typography.subtitleS.withColor(VideoTheme.colors.basePrimary) + StyleSize.S -> VideoTheme.typography.subtitleS.withColor(VideoTheme.colors.basePrimary) + StyleSize.M -> VideoTheme.typography.subtitleM.withColor(VideoTheme.colors.basePrimary) + else -> VideoTheme.typography.subtitleL.withColor(VideoTheme.colors.basePrimary) + }, + pressed: TextStyleWrapper = default, + disabled: TextStyleWrapper = default.disabledAlpha(), + ): StreamTextStyle = StreamTextStyle(default, disabled, pressed) +} + +public object StreamTextStyles : TextStyleProvider() + +// Utilities +internal fun TextStyle.wrapper(): TextStyleWrapper = TextStyleWrapper(platform = this) + +internal fun TextStyle.withColor(color: Color) = TextStyleWrapper( + platform = this.copy( + color = color, + ), +) + +internal fun TextStyleWrapper.withAlpha(alpha: Float): TextStyleWrapper = this.platform.copy( + color = this.platform.color.copy( + alpha = alpha, + ), +).wrapper() + +internal fun TextStyleWrapper.disabledAlpha() = this.withAlpha(0.16f) diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/Utils.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/Utils.kt new file mode 100644 index 0000000000..0ae74af281 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/base/styling/Utils.kt @@ -0,0 +1,38 @@ +/* + * 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.compose.ui.components.base.styling + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue + +@Composable +internal fun styleState( + interactionSource: MutableInteractionSource, + enabled: Boolean, +): StyleState { + val pressed by interactionSource.collectIsPressedAsState() + val state = if (enabled) { + StyleState.ENABLED + } else if (pressed) { + StyleState.PRESSED + } else { + StyleState.DISABLED + } + return state +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/CallAppBar.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/CallAppBar.kt index 9834eebe2a..cae418b3bc 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/CallAppBar.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/CallAppBar.kt @@ -16,10 +16,11 @@ package io.getstream.video.android.compose.ui.components.call -import android.content.res.Configuration -import android.content.res.Configuration.ORIENTATION_LANDSCAPE import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.RowScope import androidx.compose.foundation.layout.Spacer @@ -27,19 +28,16 @@ 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.CircleShape 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.VerifiedUser import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -48,14 +46,17 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.compose.ui.components.participants.ParticipantIndicatorIcon +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.GenericContainer +import io.getstream.video.android.compose.ui.components.call.controls.actions.LeaveCallAction import io.getstream.video.android.core.Call import io.getstream.video.android.core.call.state.CallAction -import io.getstream.video.android.core.call.state.ShowCallParticipantInfo +import io.getstream.video.android.core.call.state.LeaveCall import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewCall import io.getstream.video.android.ui.common.R +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.ExperimentalTime /** * Represents the default AppBar that's shown in calls. Exposes handlers for the two default slot @@ -84,49 +85,20 @@ public fun CallAppBar( DefaultCallAppBarCenterContent(call, title) }, trailingContent: (@Composable RowScope.() -> Unit)? = { - DefaultCallAppBarTrailingContent( - call = call, - onCallAction = onCallAction, - ) + LeaveCallAction { + onCallAction(LeaveCall) + } }, ) { - val orientation = LocalConfiguration.current.orientation - val height = if (orientation == ORIENTATION_LANDSCAPE) { - VideoTheme.dimens.landscapeTopAppBarHeight - } else { - VideoTheme.dimens.topAppbarHeight - } - - val endPadding = if (orientation == ORIENTATION_LANDSCAPE) { - VideoTheme.dimens.controlActionsHeight - } else { - VideoTheme.dimens.callAppBarPadding - } - Row( modifier = modifier .fillMaxWidth() - .height(height) - .background( - brush = Brush.verticalGradient( - listOf( - Color.Black.copy(alpha = 0.2f), - Color.Transparent, - ), - ), - ) - .padding( - start = VideoTheme.dimens.callAppBarPadding, - top = VideoTheme.dimens.callAppBarPadding, - bottom = VideoTheme.dimens.callAppBarPadding, - end = endPadding, - ), - verticalAlignment = Alignment.CenterVertically, + .height(VideoTheme.dimens.componentHeightL), + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { leadingContent?.invoke(this) - centerContent?.invoke(this) - trailingContent?.invoke(this) } } @@ -140,17 +112,13 @@ internal fun DefaultCallAppBarLeadingContent( ) { IconButton( onClick = onBackButtonClicked, - modifier = Modifier.padding( - start = VideoTheme.dimens.callAppBarLeadingContentSpacingStart, - end = VideoTheme.dimens.callAppBarLeadingContentSpacingEnd, - ), ) { Icon( painter = painterResource(id = R.drawable.stream_video_ic_arrow_back), contentDescription = stringResource( id = R.string.stream_video_back_button_content_description, ), - tint = VideoTheme.colors.callDescription, + tint = VideoTheme.colors.basePrimary, ) } } @@ -162,63 +130,112 @@ internal fun DefaultCallAppBarLeadingContent( internal fun RowScope.DefaultCallAppBarCenterContent(call: Call, title: String) { val isReconnecting by call.state.isReconnecting.collectAsStateWithLifecycle() val isRecording by call.state.recording.collectAsStateWithLifecycle() - - if (isRecording) { - Box( - modifier = Modifier - .size(VideoTheme.dimens.callAppBarRecordingIndicatorSize) - .align(Alignment.CenterVertically) - .clip(CircleShape) - .background(VideoTheme.colors.errorAccent), - ) - - Spacer(modifier = Modifier.width(6.dp)) - } - - Text( - modifier = Modifier - .weight(1f) - .padding( - start = VideoTheme.dimens.callAppBarCenterContentSpacingStart, - end = VideoTheme.dimens.callAppBarCenterContentSpacingEnd, - ), - text = if (isReconnecting) { - stringResource(id = R.string.stream_video_call_reconnecting) - } else if (isRecording) { - stringResource(id = R.string.stream_video_call_recording) - } else { - title - }, - fontSize = VideoTheme.dimens.topAppbarTextSize, - color = VideoTheme.colors.callDescription, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Start, + val duration by call.state.duration.collectAsStateWithLifecycle() + CalLCenterContent( + modifier = Modifier.align(CenterVertically), + text = duration?.toString() ?: title, + isRecording = isRecording, + isReconnecting = isReconnecting, ) } -/** - * Default trailing content slot, representing an icon to show the call participants menu. - */ @Composable -internal fun DefaultCallAppBarTrailingContent( - call: Call, - onCallAction: (CallAction) -> Unit, +private fun CalLCenterContent( + modifier: Modifier = Modifier, + text: String, + isRecording: Boolean, + isReconnecting: Boolean, ) { - val participants by call.state.participants.collectAsStateWithLifecycle() - - ParticipantIndicatorIcon( - number = participants.size, - onClick = { onCallAction(ShowCallParticipantInfo) }, - ) + GenericContainer(modifier = modifier) { + Row { + if (isRecording) { + Box( + modifier = Modifier + .size(VideoTheme.dimens.componentHeightS) + .clip(VideoTheme.shapes.circle) + .background( + color = VideoTheme.colors.alertWarning, + shape = VideoTheme.shapes.circle, + ) + .border(2.dp, VideoTheme.colors.basePrimary, VideoTheme.shapes.circle), + ) + } else { + Icon( + imageVector = Icons.Default.VerifiedUser, + tint = VideoTheme.colors.alertSuccess, + contentDescription = "call duration", + ) + } + Text( + modifier = Modifier + .padding( + start = VideoTheme.dimens.componentPaddingStart, + end = VideoTheme.dimens.componentPaddingEnd, + ), + text = if (isReconnecting) { + stringResource(id = R.string.stream_video_call_reconnecting) + } else if (isRecording) { + stringResource(id = R.string.stream_video_call_recording) + } else { + text + }, + fontSize = VideoTheme.dimens.textSizeS, + color = VideoTheme.colors.baseSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Start, + ) + } + } } @Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable +@ExperimentalTime private fun CallTopAppbarPreview() { StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) VideoTheme { - CallAppBar(call = previewCall) + Column { + CallAppBar(call = previewCall, centerContent = { + CalLCenterContent( + text = 100000000L.milliseconds.toString(), + isRecording = false, + isReconnecting = false, + ) + }) + Spacer(modifier = Modifier.size(16.dp)) + CallAppBar(call = previewCall, centerContent = { + CalLCenterContent( + text = 100000000L.milliseconds.toString(), + isRecording = true, + isReconnecting = false, + ) + }) + Spacer(modifier = Modifier.size(16.dp)) + CallAppBar(call = previewCall, centerContent = { + CalLCenterContent( + text = 100000000L.milliseconds.toString(), + isRecording = false, + isReconnecting = false, + ) + }) + Spacer(modifier = Modifier.size(16.dp)) + CallAppBar(call = previewCall, centerContent = { + CalLCenterContent( + text = 100000000L.milliseconds.toString(), + isRecording = false, + isReconnecting = true, + ) + }) + Spacer(modifier = Modifier.size(16.dp)) + CallAppBar(call = previewCall, centerContent = { + CalLCenterContent( + text = 100000000L.milliseconds.toString(), + isRecording = true, + isReconnecting = false, + ) + }) + Spacer(modifier = Modifier.size(16.dp)) + } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt index 97ab9e1e86..5650fb15c3 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt @@ -16,7 +16,8 @@ package io.getstream.video.android.compose.ui.components.call.activecall -import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.content.res.Configuration.ORIENTATION_PORTRAIT +import android.content.res.Configuration.UI_MODE_TYPE_CAR import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures @@ -30,11 +31,8 @@ import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.material.IconButton +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material.Scaffold -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.AutoAwesomeMosaic import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -49,7 +47,6 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.coerceAtLeast @@ -61,7 +58,7 @@ import io.getstream.video.android.compose.permission.VideoPermissionsState import io.getstream.video.android.compose.permission.rememberCallPermissionsState import io.getstream.video.android.compose.pip.enterPictureInPicture import io.getstream.video.android.compose.pip.isInPictureInPictureMode -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.CallAppBar import io.getstream.video.android.compose.ui.components.call.controls.ControlActions import io.getstream.video.android.compose.ui.components.call.controls.actions.DefaultOnCallActionHandler @@ -76,10 +73,8 @@ import io.getstream.video.android.compose.ui.components.video.VideoRenderer import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState import io.getstream.video.android.core.call.state.CallAction -import io.getstream.video.android.core.call.state.ChooseLayout import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewCall -import io.getstream.video.android.ui.common.R /** * Represents the UI in an Active call that shows participants and their video, as well as some @@ -106,16 +101,13 @@ public fun CallContent( call: Call, modifier: Modifier = Modifier, layout: LayoutType = LayoutType.DYNAMIC, - isShowingOverlayAppBar: Boolean = true, + isShowingOverlayAppBar: Boolean = false, permissions: VideoPermissionsState = rememberCallPermissionsState(call = call), onBackPressed: () -> Unit = {}, onCallAction: (CallAction) -> Unit = { DefaultOnCallActionHandler.onCallAction(call, it) }, appBarContent: @Composable (call: Call) -> Unit = { CallAppBar( call = call, - leadingContent = { - LayoutChoiceLeadingContent(onCallAction) - }, onCallAction = onCallAction, ) }, @@ -141,7 +133,7 @@ public fun CallContent( modifier = Modifier .fillMaxSize() .weight(1f) - .padding(bottom = VideoTheme.dimens.participantsGridPadding), + .padding(bottom = VideoTheme.dimens.spacingXXs), style = style, videoRenderer = videoRenderer, floatingVideoRenderer = floatingVideoRenderer, @@ -150,6 +142,7 @@ public fun CallContent( videoOverlayContent: @Composable (call: Call) -> Unit = {}, controlsContent: @Composable (call: Call) -> Unit = { ControlActions( + modifier = Modifier.wrapContentWidth(), call = call, onCallAction = onCallAction, ) @@ -186,28 +179,35 @@ public fun CallContent( pictureInPictureContent(call) } else { Scaffold( - modifier = modifier, - contentColor = VideoTheme.colors.appBackground, - topBar = { }, + backgroundColor = VideoTheme.colors.baseSheetPrimary, + contentColor = VideoTheme.colors.baseSheetPrimary, + topBar = { + if (orientation == ORIENTATION_PORTRAIT) { + appBarContent.invoke(call) + } + }, bottomBar = { - if (orientation != ORIENTATION_LANDSCAPE) { + if (orientation == ORIENTATION_PORTRAIT) { controlsContent.invoke(call) } }, content = { val paddings = PaddingValues( - top = it.calculateTopPadding(), + top = (it.calculateTopPadding() - VideoTheme.dimens.spacingS) + .coerceAtLeast(0.dp), start = it.calculateStartPadding( layoutDirection = LocalLayoutDirection.current, ), - end = it.calculateEndPadding(layoutDirection = LocalLayoutDirection.current), - bottom = (it.calculateBottomPadding() - VideoTheme.dimens.controlActionsBottomPadding) + end = it.calculateEndPadding( + layoutDirection = LocalLayoutDirection.current, + ), + bottom = (it.calculateBottomPadding() - VideoTheme.dimens.spacingS) .coerceAtLeast(0.dp), ) var showDiagnostics by remember { mutableStateOf(false) } Row( modifier = modifier - .background(color = VideoTheme.colors.appBackground) + .background(color = VideoTheme.colors.baseSheetPrimary) .padding(paddings) .pointerInput(Unit) { detectTapGestures( @@ -225,14 +225,6 @@ public fun CallContent( videoOverlayContent.invoke(call) } } - - if (orientation == ORIENTATION_LANDSCAPE) { - controlsContent.invoke(call) - } - } - - if (isShowingOverlayAppBar) { - appBarContent.invoke(call) } if (enableDiagnostics && showDiagnostics) { @@ -245,25 +237,6 @@ public fun CallContent( } } -@Composable -internal fun LayoutChoiceLeadingContent(onCallAction: (CallAction) -> Unit) { - IconButton( - onClick = { onCallAction.invoke(ChooseLayout) }, - modifier = Modifier.padding( - start = VideoTheme.dimens.callAppBarLeadingContentSpacingStart, - end = VideoTheme.dimens.callAppBarLeadingContentSpacingEnd, - ), - ) { - Icon( - imageVector = Icons.Rounded.AutoAwesomeMosaic, - contentDescription = stringResource( - id = R.string.stream_video_back_button_content_description, - ), - tint = VideoTheme.colors.callDescription, - ) - } -} - /** * Renders the default PiP content, using the call state that's provided. * @@ -321,3 +294,16 @@ private fun CallContentPreview() { CallContent(call = previewCall) } } + +@Preview( + widthDp = 640, + heightDp = 360, + uiMode = UI_MODE_TYPE_CAR, +) +@Composable +private fun CallContentPreviewLandscape() { + StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + CallContent(call = previewCall) + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/internal/InviteUsersDialog.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/internal/InviteUsersDialog.kt index 828c6db8e4..9b3e6fb8f7 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/internal/InviteUsersDialog.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/internal/InviteUsersDialog.kt @@ -16,24 +16,14 @@ package io.getstream.video.android.compose.ui.components.call.activecall.internal -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.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.GroupAdd import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.End -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import io.getstream.video.android.compose.theme.VideoTheme +import androidx.compose.ui.tooling.preview.Preview +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamDialogPositiveNegative +import io.getstream.video.android.mock.previewUsers import io.getstream.video.android.model.User import io.getstream.video.android.ui.common.R @@ -44,65 +34,38 @@ import io.getstream.video.android.ui.common.R * @param onDismiss Handler when the user wants to dismiss and cancel the operation. * @param onInviteUsers Handler when the user wants to confirm invites. */ +// TODO AAP: Move into demo-app @Composable internal fun InviteUsersDialog( users: List, onDismiss: () -> Unit, onInviteUsers: (List) -> Unit, ) { - Dialog(onDismissRequest = onDismiss, content = { - Surface( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - shape = VideoTheme.shapes.dialog, - color = VideoTheme.colors.appBackground, + StreamDialogPositiveNegative( + icon = Icons.Default.GroupAdd, + title = stringResource(R.string.stream_video_invite_users_title), + positiveButton = Triple( + stringResource(R.string.stream_video_invite_users_accept), + VideoTheme.styles.buttonStyles.secondaryButtonStyle(), ) { - Column(modifier = Modifier.fillMaxWidth()) { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(top = 32.dp), - text = stringResource(R.string.stream_video_invite_users_title), - style = VideoTheme.typography.bodyBold, - color = VideoTheme.colors.textHighEmphasis, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - modifier = Modifier.padding(start = 16.dp, end = 16.dp), - text = stringResource(R.string.stream_video_invite_users_message, users.size), - style = VideoTheme.typography.body, - color = VideoTheme.colors.textHighEmphasis, - ) - - Spacer(modifier = Modifier.height(32.dp)) - - Row( - modifier = Modifier.align(End), - verticalAlignment = Alignment.CenterVertically, - ) { - TextButton( - onClick = onDismiss, - colors = ButtonDefaults.textButtonColors( - contentColor = VideoTheme.colors.primaryAccent, - ), - ) { - Text(text = stringResource(R.string.stream_video_invite_users_cancel)) - } + onInviteUsers(users) + }, + negativeButton = Triple( + stringResource(R.string.stream_video_invite_users_cancel), + VideoTheme.styles.buttonStyles.tetriaryButtonStyle(), + ) { + onDismiss() + }, + contentText = stringResource(R.string.stream_video_invite_users_message, users.size), + style = VideoTheme.styles.dialogStyles.defaultDialogStyle(), + ) +} - TextButton( - onClick = { onInviteUsers(users) }, - colors = ButtonDefaults.textButtonColors( - contentColor = VideoTheme.colors.primaryAccent, - ), - ) { - Text(text = stringResource(R.string.stream_video_invite_users_accept)) - } - } - } +@Preview +@Composable +private fun InviteUsersDialogPreview() { + VideoTheme { + InviteUsersDialog(users = previewUsers, onDismiss = { }) { } - }) + } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/ControlActions.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/ControlActions.kt index 3f9d4108e5..3e8e4e8838 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/ControlActions.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/ControlActions.kt @@ -16,25 +16,18 @@ package io.getstream.video.android.compose.ui.components.call.controls -import android.content.res.Configuration.ORIENTATION_LANDSCAPE -import android.content.res.Configuration.ORIENTATION_PORTRAIT +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.call.controls.actions.DefaultOnCallActionHandler -import io.getstream.video.android.compose.ui.components.call.controls.actions.LandscapeControlActions -import io.getstream.video.android.compose.ui.components.call.controls.actions.RegularControlActions import io.getstream.video.android.compose.ui.components.call.controls.actions.buildDefaultCallControlActions import io.getstream.video.android.core.Call import io.getstream.video.android.core.call.state.CallAction @@ -56,49 +49,25 @@ public fun ControlActions( call: Call, modifier: Modifier = Modifier, onCallAction: (CallAction) -> Unit = { DefaultOnCallActionHandler.onCallAction(call, it) }, - backgroundColor: Color = VideoTheme.colors.barsBackground, - elevation: Dp = VideoTheme.dimens.controlActionsElevation, - shape: Shape = VideoTheme.shapes.callControls, - spaceBy: Dp? = null, actions: List<(@Composable () -> Unit)> = buildDefaultCallControlActions( call = call, onCallAction, ), ) { - val orientation = LocalConfiguration.current.orientation - - val controlsModifier = if (orientation == ORIENTATION_LANDSCAPE) { - modifier - .fillMaxHeight() - .width(VideoTheme.dimens.landscapeControlActionsWidth) - } else { - modifier - .fillMaxWidth() - .height(VideoTheme.dimens.controlActionsHeight) - } - - if (orientation == ORIENTATION_PORTRAIT) { - RegularControlActions( - modifier = controlsModifier, - call = call, - backgroundColor = backgroundColor, - shape = shape, - elevation = elevation, - spaceBy = spaceBy, - onCallAction = onCallAction, - actions = actions, - ) - } else if (orientation == ORIENTATION_LANDSCAPE) { - LandscapeControlActions( - modifier = controlsModifier, - call = call, - backgroundColor = backgroundColor, - shape = shape, - elevation = elevation, - spaceBy = spaceBy, - onCallAction = onCallAction, - actions = actions, - ) + Box( + modifier = modifier, + ) { + LazyRow( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + VideoTheme.dimens.spacingM, + Alignment.CenterHorizontally, + ), + ) { + items(actions) { action -> + action.invoke() + } + } } } @@ -110,8 +79,5 @@ private fun CallControlsPreview() { VideoTheme { ControlActions(call = previewCall, onCallAction = {}) } - VideoTheme(isInDarkMode = true) { - ControlActions(call = previewCall, onCallAction = {}) - } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/AcceptCallAction.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/AcceptCallAction.kt index aee2b702f4..4b437f0437 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/AcceptCallAction.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/AcceptCallAction.kt @@ -16,19 +16,16 @@ package io.getstream.video.android.compose.ui.components.call.controls.actions -import androidx.compose.foundation.background -import androidx.compose.material.Icon -import androidx.compose.material.IconButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Call import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import io.getstream.video.android.compose.theme.VideoTheme +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.call.state.AcceptCall import io.getstream.video.android.core.call.state.CallAction -import io.getstream.video.android.ui.common.R /** * A call action button represents accepting a call. @@ -41,24 +38,23 @@ import io.getstream.video.android.ui.common.R public fun AcceptCallAction( modifier: Modifier = Modifier, enabled: Boolean = true, - shape: Shape = VideoTheme.shapes.callButton, + icon: ImageVector? = null, + bgColor: Color? = null, + iconTint: Color? = null, onCallAction: (AcceptCall) -> Unit, -) { - IconButton( - modifier = modifier.background( - color = VideoTheme.colors.infoAccent, - shape = shape, - ), - enabled = enabled, - onClick = { onCallAction(AcceptCall) }, - content = { - Icon( - painter = painterResource(id = R.drawable.stream_video_ic_call), - tint = Color.White, - contentDescription = stringResource( - R.string.stream_video_call_controls_accept_call, - ), - ) - }, - ) +): Unit = GenericAction( + modifier = modifier, + enabled = enabled, + onAction = { onCallAction(AcceptCall) }, + icon = icon ?: Icons.Default.Call, + color = bgColor ?: VideoTheme.colors.alertSuccess, + iconTint = iconTint ?: VideoTheme.colors.basePrimary, +) + +@Preview +@Composable +private fun AcceptCallActionPreview() { + VideoTheme { + AcceptCallAction(onCallAction = {}) + } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/AudioControlsActions.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/AudioControlsActions.kt deleted file mode 100644 index dd7bed8add..0000000000 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/AudioControlsActions.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.compose.ui.components.call.controls.actions - -import android.content.res.Configuration -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalInspectionMode -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.state.CallAction - -/** - * Builds the default set of Call Control actions based on the call devices. - * - * @param call The call that contains all the participants state and tracks. - * @return [List] of call control actions that the user can trigger. - */ -@Composable -public fun buildDefaultAudioControlActions( - call: Call, - onCallAction: (CallAction) -> Unit, -): List<@Composable () -> Unit> { - val orientation = LocalConfiguration.current.orientation - - val modifier = if (orientation == Configuration.ORIENTATION_PORTRAIT) { - Modifier.size(VideoTheme.dimens.controlActionsButtonSize) - } else { - Modifier.size(VideoTheme.dimens.landscapeControlActionsButtonSize) - } - - val isMicrophoneEnabled by if (LocalInspectionMode.current) { - remember { mutableStateOf(true) } - } else { - call.microphone.isEnabled.collectAsStateWithLifecycle() - } - - return listOf( - { - ToggleMicrophoneAction( - modifier = modifier, - isMicrophoneEnabled = isMicrophoneEnabled, - onCallAction = onCallAction, - ) - }, - { - LeaveCallAction( - modifier = modifier, - onCallAction = onCallAction, - ) - }, - ) -} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/CallControlActionBackground.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/CallControlActionBackground.kt deleted file mode 100644 index 026a9b38e4..0000000000 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/CallControlActionBackground.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.compose.ui.components.call.controls.actions - -import androidx.compose.material.Card -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import io.getstream.video.android.compose.theme.VideoTheme - -@Composable -public fun CallControlActionBackground( - modifier: Modifier = Modifier, - isEnabled: Boolean, - enabledColor: Color = VideoTheme.colors.callActionIconEnabledBackground, - disabledColor: Color = VideoTheme.colors.callActionIconDisabledBackground, - shape: Shape = VideoTheme.shapes.callControlsButton, - content: @Composable () -> Unit, -) { - Card( - modifier = modifier, - shape = shape, - backgroundColor = if (isEnabled) { - enabledColor - } else { - disabledColor - }, - content = content, - ) -} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/CancelCallAction.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/CancelCallAction.kt index e4f0e243af..0ab9a5fd13 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/CancelCallAction.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/CancelCallAction.kt @@ -16,19 +16,16 @@ package io.getstream.video.android.compose.ui.components.call.controls.actions -import androidx.compose.foundation.background -import androidx.compose.material.Icon -import androidx.compose.material.IconButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Call import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import io.getstream.video.android.compose.theme.VideoTheme +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.call.state.CallAction import io.getstream.video.android.core.call.state.CancelCall -import io.getstream.video.android.ui.common.R /** * A call action button represents canceling a call. @@ -41,24 +38,23 @@ import io.getstream.video.android.ui.common.R public fun CancelCallAction( modifier: Modifier = Modifier, enabled: Boolean = true, + icon: ImageVector? = null, + bgColor: Color? = null, + iconTint: Color? = null, onCallAction: (CancelCall) -> Unit, - shape: Shape = VideoTheme.shapes.callButton, -) { - IconButton( - modifier = modifier.background( - color = VideoTheme.colors.errorAccent, - shape = shape, - ), - enabled = enabled, - onClick = { onCallAction(CancelCall) }, - content = { - Icon( - painter = painterResource(id = R.drawable.stream_video_ic_call_end), - tint = Color.White, - contentDescription = stringResource( - R.string.stream_video_call_controls_cancel_call, - ), - ) - }, - ) +): Unit = GenericAction( + modifier = modifier, + enabled = enabled, + onAction = { onCallAction(CancelCall) }, + icon = icon ?: Icons.Default.Call, + color = bgColor ?: VideoTheme.colors.alertWarning, + iconTint = iconTint ?: VideoTheme.colors.basePrimary, +) + +@Preview +@Composable +private fun CancelCallActionPreview() { + VideoTheme { + CancelCallAction(onCallAction = {}) + } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ChatDialogAction.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ChatDialogAction.kt index 9f62a93daa..fbfe0eedb0 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ChatDialogAction.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ChatDialogAction.kt @@ -16,20 +16,17 @@ package io.getstream.video.android.compose.ui.components.call.controls.actions -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.QuestionAnswer import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamBadgeBox import io.getstream.video.android.core.call.state.CallAction -import io.getstream.video.android.core.call.state.ChatDialog -import io.getstream.video.android.ui.common.R +import io.getstream.video.android.core.call.state.CancelCall /** * A call action button represents displaying a chat dialog. @@ -42,25 +39,36 @@ import io.getstream.video.android.ui.common.R public fun ChatDialogAction( modifier: Modifier = Modifier, enabled: Boolean = true, - shape: Shape = VideoTheme.shapes.callControlsButton, - enabledColor: Color = VideoTheme.colors.callActionIconEnabledBackground, - disabledColor: Color = VideoTheme.colors.callActionIconDisabledBackground, - onCallAction: (ChatDialog) -> Unit, + messageCount: Int? = null, + icon: ImageVector? = null, + bgColor: Color? = null, + iconTint: Color? = null, + badgeColor: Color? = null, + onCallAction: (CancelCall) -> Unit, +): Unit = StreamBadgeBox( + showWithoutValue = false, + style = VideoTheme.styles.badgeStyles.defaultBadgeStyle().copy( + color = badgeColor ?: VideoTheme.colors.alertSuccess, + ), + text = messageCount?.toString(), ) { - CallControlActionBackground( + GenericAction( modifier = modifier, - isEnabled = enabled, - shape = shape, - enabledColor = enabledColor, - disabledColor = disabledColor, - ) { - Icon( - modifier = Modifier - .padding(13.dp) - .clickable(enabled = enabled) { onCallAction(ChatDialog) }, - tint = VideoTheme.colors.callActionIconEnabled, - painter = painterResource(id = R.drawable.stream_video_ic_message), - contentDescription = stringResource(R.string.stream_video_call_controls_chat_dialog), - ) + enabled = enabled, + onAction = { onCallAction(CancelCall) }, + icon = icon ?: Icons.Default.QuestionAnswer, + color = bgColor, + iconTint = iconTint, + ) +} + +@Preview +@Composable +private fun ChatActionPreview() { + VideoTheme { + ChatDialogAction { + } + ChatDialogAction(messageCount = 15) { + } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/CallControlsActions.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ControlActionsBuilder.kt similarity index 80% rename from stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/CallControlsActions.kt rename to stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ControlActionsBuilder.kt index 271bd81e29..c176d06cff 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/CallControlsActions.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ControlActionsBuilder.kt @@ -16,17 +16,12 @@ package io.getstream.video.android.compose.ui.components.call.controls.actions -import android.content.res.Configuration -import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalInspectionMode 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.state.CallAction import io.getstream.video.android.core.call.state.FlipCamera @@ -41,18 +36,40 @@ import io.getstream.video.android.core.call.state.ToggleSpeakerphone * @return [List] of call control actions that the user can trigger. */ @Composable -public fun buildDefaultCallControlActions( +public fun buildDefaultAudioControlActions( call: Call, onCallAction: (CallAction) -> Unit, ): List<@Composable () -> Unit> { - val orientation = LocalConfiguration.current.orientation - - val modifier = if (orientation == Configuration.ORIENTATION_PORTRAIT) { - Modifier.size(VideoTheme.dimens.controlActionsButtonSize) + val isMicrophoneEnabled by if (LocalInspectionMode.current) { + remember { mutableStateOf(true) } } else { - Modifier.size(VideoTheme.dimens.landscapeControlActionsButtonSize) + call.microphone.isEnabled.collectAsStateWithLifecycle() } + return listOf( + { + ToggleMicrophoneAction( + isMicrophoneEnabled = isMicrophoneEnabled, + onCallAction = onCallAction, + ) + }, + { + LeaveCallAction(onCallAction = onCallAction) + }, + ) +} + +/** + * Builds the default set of Call Control actions based on the call devices. + * + * @param call The call that contains all the participants state and tracks. + * @return [List] of call control actions that the user can trigger. + */ +@Composable +public fun buildDefaultCallControlActions( + call: Call, + onCallAction: (CallAction) -> Unit, +): List<@Composable () -> Unit> { val isCameraEnabled by if (LocalInspectionMode.current) { remember { mutableStateOf(true) } } else { @@ -67,27 +84,18 @@ public fun buildDefaultCallControlActions( return listOf( { ToggleCameraAction( - modifier = modifier, isCameraEnabled = isCameraEnabled, onCallAction = onCallAction, ) }, { ToggleMicrophoneAction( - modifier = modifier, isMicrophoneEnabled = isMicrophoneEnabled, onCallAction = onCallAction, ) }, { FlipCameraAction( - modifier = modifier, - onCallAction = onCallAction, - ) - }, - { - LeaveCallAction( - modifier = modifier, onCallAction = onCallAction, ) }, diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/DeclineCallAction.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/DeclineCallAction.kt index ed850c0672..f74103a1d6 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/DeclineCallAction.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/DeclineCallAction.kt @@ -16,19 +16,16 @@ package io.getstream.video.android.compose.ui.components.call.controls.actions -import androidx.compose.foundation.background -import androidx.compose.material.Icon -import androidx.compose.material.IconButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Call import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import io.getstream.video.android.compose.theme.VideoTheme +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.call.state.CallAction import io.getstream.video.android.core.call.state.DeclineCall -import io.getstream.video.android.ui.common.R /** * A call action button represents canceling a call. @@ -42,23 +39,22 @@ public fun DeclineCallAction( modifier: Modifier = Modifier, enabled: Boolean = true, onCallAction: (DeclineCall) -> Unit, - shape: Shape = VideoTheme.shapes.callButton, -) { - IconButton( - modifier = modifier.background( - color = VideoTheme.colors.errorAccent, - shape = shape, - ), - enabled = enabled, - onClick = { onCallAction(DeclineCall) }, - content = { - Icon( - painter = painterResource(id = R.drawable.stream_video_ic_call_end), - tint = Color.White, - contentDescription = stringResource( - R.string.stream_video_call_controls_cancel_call, - ), - ) - }, - ) + icon: ImageVector? = null, + bgColor: Color? = null, + iconTint: Color? = null, +): Unit = GenericAction( + modifier = modifier, + enabled = enabled, + onAction = { onCallAction(DeclineCall) }, + icon = icon ?: Icons.Default.Call, + color = bgColor ?: VideoTheme.colors.alertWarning, + iconTint = iconTint ?: VideoTheme.colors.basePrimary, +) + +@Preview +@Composable +private fun DeclineCallActionPreview() { + VideoTheme { + DeclineCallAction(onCallAction = {}) + } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/FlipCameraAction.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/FlipCameraAction.kt index 959a383b41..505125e43b 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/FlipCameraAction.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/FlipCameraAction.kt @@ -16,20 +16,15 @@ package io.getstream.video.android.compose.ui.components.call.controls.actions -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FlipCameraIos import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import androidx.compose.ui.tooling.preview.Preview +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.call.state.CallAction import io.getstream.video.android.core.call.state.FlipCamera -import io.getstream.video.android.ui.common.R /** * A call action button represents flipping a camera. @@ -42,25 +37,24 @@ import io.getstream.video.android.ui.common.R public fun FlipCameraAction( modifier: Modifier = Modifier, enabled: Boolean = true, - shape: Shape = VideoTheme.shapes.callControlsButton, - enabledColor: Color = VideoTheme.colors.callActionIconEnabledBackground, - disabledColor: Color = VideoTheme.colors.callActionIconDisabledBackground, + color: Color? = null, + iconTint: Color? = null, onCallAction: (FlipCamera) -> Unit, +): Unit = GenericAction( + modifier = modifier, + enabled = enabled, + color = color, + iconTint = iconTint, + icon = Icons.Default.FlipCameraIos, ) { - CallControlActionBackground( - modifier = modifier, - isEnabled = true, - shape = shape, - enabledColor = enabledColor, - disabledColor = disabledColor, - ) { - Icon( - modifier = Modifier - .padding(13.dp) - .clickable(enabled = enabled) { onCallAction(FlipCamera) }, - tint = VideoTheme.colors.callActionIconEnabled, - painter = painterResource(id = R.drawable.stream_video_ic_camera_flip), - contentDescription = stringResource(R.string.stream_video_call_controls_flip_camera), - ) + onCallAction(FlipCamera) +} + +@Preview +@Composable +private fun FlipActionPreview() { + VideoTheme { + FlipCameraAction { + } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/GenericActions.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/GenericActions.kt new file mode 100644 index 0000000000..7cffc4b382 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/GenericActions.kt @@ -0,0 +1,133 @@ +/* + * 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.compose.ui.components.call.controls.actions + +import androidx.compose.material.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.state.ToggleableState +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamIconButton +import io.getstream.video.android.compose.ui.components.base.StreamIconToggleButton +import io.getstream.video.android.compose.ui.components.base.styling.StreamFixedSizeButtonStyle +import io.getstream.video.android.core.call.state.CallAction + +@Composable +public fun GenericAction( + modifier: Modifier = Modifier, + icon: ImageVector, + enabled: Boolean = true, + shape: Shape? = null, + color: Color? = null, + iconTint: Color? = null, + style: StreamFixedSizeButtonStyle? = null, + onAction: () -> Unit, +): Unit = StreamIconButton( + modifier = modifier, + enabled = enabled, + icon = icon, + style = style ?: VideoTheme.styles.buttonStyles.primaryIconButtonStyle() + .let { + it.copyFixed( + shape = shape ?: it.shape, + colors = color?.let { + ButtonDefaults.buttonColors( + backgroundColor = color, + disabledBackgroundColor = color.copy(alpha = 0.16f), + ) + } + ?: it.colors, + iconStyle = it.iconStyle.copy( + default = it.iconStyle.default.copy( + color = iconTint ?: it.iconStyle.default.color, + ), + ), + ) + }, + onClick = { + onAction() + }, +) + +/** + * A call action button represents toggling a microphone. + * + * @param modifier Optional Modifier for this action button. + * @param isMicrophoneEnabled Represent is camera enabled. + * @param enabled Whether or not this action button will handle input events. + * @param onCallAction A [CallAction] event that will be fired. + */ +@Composable +public fun ToggleAction( + modifier: Modifier = Modifier, + isActionActive: Boolean, + iconOnOff: Pair, + enabled: Boolean = true, + shape: Shape? = null, + enabledColor: Color? = null, + disabledColor: Color? = null, + enabledIconTint: Color? = null, + disabledIconTint: Color? = null, + progress: Boolean = false, + onStyle: StreamFixedSizeButtonStyle? = null, + offStyle: StreamFixedSizeButtonStyle? = null, + onAction: () -> Unit, +): Unit = StreamIconToggleButton( + modifier = modifier, + enabled = enabled, + showProgress = progress, + toggleState = rememberUpdatedState(newValue = ToggleableState(isActionActive)), + onIcon = iconOnOff.first, + offIcon = iconOnOff.second, + onStyle = onStyle ?: VideoTheme.styles.buttonStyles.primaryIconButtonStyle() + .let { + it.copyFixed( + shape = shape ?: it.shape, + colors = enabledColor?.let { ButtonDefaults.buttonColors(backgroundColor = enabledColor) } + ?: it.colors, + iconStyle = it.iconStyle.copy( + default = it.iconStyle.default.copy( + color = enabledIconTint ?: it.iconStyle.default.color, + ), + ), + ) + }, + offStyle = offStyle ?: VideoTheme.styles.buttonStyles.alertIconButtonStyle().let { + it.copyFixed( + shape = shape ?: it.shape, + colors = disabledColor?.let { + ButtonDefaults.buttonColors( + backgroundColor = disabledColor, + disabledBackgroundColor = disabledColor, + ) + } + ?: it.colors, + iconStyle = it.iconStyle.copy( + default = it.iconStyle.default.copy( + color = disabledIconTint ?: it.iconStyle.default.color, + ), + ), + ) + }, + onClick = { + onAction() + }, +) diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/LandscapeControlActions.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/LandscapeControlActions.kt deleted file mode 100644 index 90ad28df0c..0000000000 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/LandscapeControlActions.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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.compose.ui.components.call.controls.actions - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.core.Call -import io.getstream.video.android.core.call.state.CallAction -import io.getstream.video.android.mock.StreamPreviewDataUtils -import io.getstream.video.android.mock.previewCall - -/** - * Shows the call controls in a different way when in landscape mode. - * - * @param call The call that contains all the participants state and tracks. - * @param modifier Modifier for styling. - * @param actions Actions to show to the user with different controls. - * @param onCallAction Handler when the user triggers various call actions. - */ -@Composable -public fun LandscapeControlActions( - call: Call, - modifier: Modifier = Modifier, - backgroundColor: Color = VideoTheme.colors.barsBackground, - shape: Shape = VideoTheme.shapes.callControlsLandscape, - elevation: Dp = VideoTheme.dimens.controlActionsElevation, - spaceBy: Dp? = null, - onCallAction: (CallAction) -> Unit = { DefaultOnCallActionHandler.onCallAction(call, it) }, - actions: List<(@Composable () -> Unit)> = buildDefaultCallControlActions( - call, - onCallAction, - ), -) { - Surface( - modifier = modifier, - shape = shape, - color = backgroundColor, - elevation = elevation, - ) { - LazyColumn( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = if (spaceBy != null) { - Arrangement.spacedBy(space = spaceBy, alignment = Alignment.CenterVertically) - } else { - Arrangement.SpaceEvenly - }, - ) { - items(actions) { action -> - action.invoke() - } - } - } -} - -@Preview -@Composable -private fun LandscapeCallControlsPreview() { - StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) - VideoTheme { - LandscapeControlActions( - call = previewCall, - onCallAction = {}, - ) - } -} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/LeaveCallAction.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/LeaveCallAction.kt index f1645d5b56..32c9ba8a8c 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/LeaveCallAction.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/LeaveCallAction.kt @@ -16,19 +16,15 @@ package io.getstream.video.android.compose.ui.components.call.controls.actions -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CallEnd import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import androidx.compose.ui.tooling.preview.Preview +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.call.state.CallAction import io.getstream.video.android.core.call.state.LeaveCall -import io.getstream.video.android.ui.common.R /** * A call action button represents leaving a call. @@ -42,19 +38,26 @@ public fun LeaveCallAction( modifier: Modifier = Modifier, enabled: Boolean = true, onCallAction: (LeaveCall) -> Unit, +): Unit = ToggleAction( + modifier = modifier, + enabled = enabled, + isActionActive = false, + onStyle = VideoTheme.styles.buttonStyles.alertIconButtonStyle(), + offStyle = VideoTheme.styles.buttonStyles.alertIconButtonStyle(), + iconOnOff = Pair(Icons.Default.CallEnd, Icons.Default.CallEnd), ) { - CallControlActionBackground( - modifier = modifier, - isEnabled = true, - enabledColor = VideoTheme.colors.errorAccent, - ) { - Icon( - modifier = Modifier - .padding(12.dp) - .clickable(enabled = enabled) { onCallAction(LeaveCall) }, - tint = Color.White, - painter = painterResource(id = R.drawable.stream_video_ic_call_end), - contentDescription = stringResource(R.string.stream_video_call_controls_leave_call), - ) + onCallAction(LeaveCall) +} + +@Preview +@Composable +public fun LeaveCalLActionPreview() { + VideoTheme { + Row { + LeaveCallAction(enabled = false) { + } + LeaveCallAction { + } + } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ReactionAction.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ReactionAction.kt deleted file mode 100644 index 0bcdb2cc78..0000000000 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ReactionAction.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.compose.ui.components.call.controls.actions - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.core.call.state.CallAction -import io.getstream.video.android.core.call.state.Reaction -import io.getstream.video.android.ui.common.R - -/** - * A call action button represents reaction actions. - * - * @param modifier Optional Modifier for this action button. - * @param enabled Whether or not this action button will handle input events. - * @param onCallAction A [CallAction] event that will be fired. - */ -@Composable -public fun ReactionAction( - modifier: Modifier = Modifier, - enabled: Boolean = true, - shape: Shape = VideoTheme.shapes.callControlsButton, - enabledColor: Color = VideoTheme.colors.callActionIconEnabledBackground, - disabledColor: Color = VideoTheme.colors.callActionIconDisabledBackground, - onCallAction: (Reaction) -> Unit, -) { - CallControlActionBackground( - modifier = modifier, - isEnabled = true, - shape = shape, - enabledColor = enabledColor, - disabledColor = disabledColor, - ) { - Icon( - modifier = Modifier - .padding(12.dp) - .clickable(enabled = enabled) { onCallAction(Reaction) }, - painter = painterResource(id = R.drawable.stream_video_ic_reaction), - contentDescription = stringResource(R.string.stream_video_call_controls_reaction), - ) - } -} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/RegularControlActions.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/RegularControlActions.kt deleted file mode 100644 index 4e5c2a1f28..0000000000 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/RegularControlActions.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.compose.ui.components.call.controls.actions - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.material.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.core.Call -import io.getstream.video.android.core.call.state.CallAction -import io.getstream.video.android.mock.StreamPreviewDataUtils -import io.getstream.video.android.mock.previewCall - -/** - * Represents the set of controls the user can use to change their audio and video device state, or - * browse other types of settings, leave the call, or implement something custom. - * - * @param call The call that contains all the participants state and tracks. - * @param modifier Modifier for styling. - * @param actions Actions to show to the user with different controls. - * @param onCallAction Handler when the user triggers an action. - */ -@Composable -public fun RegularControlActions( - call: Call, - modifier: Modifier = Modifier, - backgroundColor: Color = VideoTheme.colors.barsBackground, - shape: Shape = VideoTheme.shapes.callControls, - elevation: Dp = VideoTheme.dimens.controlActionsElevation, - spaceBy: Dp? = null, - onCallAction: (CallAction) -> Unit = { DefaultOnCallActionHandler.onCallAction(call, it) }, - actions: List<(@Composable () -> Unit)> = buildDefaultCallControlActions( - call, - onCallAction, - ), -) { - Surface( - modifier = modifier, - shape = shape, - color = backgroundColor, - elevation = elevation, - ) { - LazyRow( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = if (spaceBy != null) { - Arrangement.spacedBy(space = spaceBy, alignment = Alignment.CenterHorizontally) - } else { - Arrangement.SpaceEvenly - }, - ) { - items(actions) { action -> - action.invoke() - } - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun RegularCallControlsActionsPreview() { - StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) - VideoTheme { - RegularControlActions( - call = previewCall, - onCallAction = {}, - ) - } -} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/SettingsAction.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/SettingsAction.kt index d5d074de87..44338e98cc 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/SettingsAction.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/SettingsAction.kt @@ -16,20 +16,18 @@ package io.getstream.video.android.compose.ui.components.call.controls.actions -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import androidx.compose.ui.tooling.preview.Preview +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.call.state.CallAction import io.getstream.video.android.core.call.state.Settings -import io.getstream.video.android.ui.common.R /** * A call action button represents displaying a chat dialog. @@ -39,28 +37,39 @@ import io.getstream.video.android.ui.common.R * @param onCallAction A [CallAction] event that will be fired. */ @Composable -public fun SettingsAction( +public fun ToggleSettingsAction( modifier: Modifier = Modifier, + isShowingSettings: Boolean, enabled: Boolean = true, - shape: Shape = VideoTheme.shapes.callControlsButton, - enabledColor: Color = VideoTheme.colors.callActionIconEnabledBackground, - disabledColor: Color = VideoTheme.colors.callActionIconDisabledBackground, + shape: Shape? = null, + enabledColor: Color? = null, + disabledColor: Color? = null, onCallAction: (Settings) -> Unit, +): Unit = ToggleAction( + isActionActive = isShowingSettings, + iconOnOff = + Pair(Icons.Default.MoreVert, Icons.Default.MoreVert), + modifier = modifier, + enabled = enabled, shape = shape, + enabledColor = enabledColor, disabledColor = disabledColor, + offStyle = VideoTheme.styles.buttonStyles.secondaryIconButtonStyle(), + onStyle = VideoTheme.styles.buttonStyles.primaryIconButtonStyle(), ) { - CallControlActionBackground( - modifier = modifier, - isEnabled = enabled, - shape = shape, - enabledColor = enabledColor, - disabledColor = disabledColor, - ) { - Icon( - modifier = Modifier - .padding(13.dp) - .clickable(enabled = enabled) { onCallAction(Settings) }, - tint = VideoTheme.colors.callActionIconEnabled, - painter = painterResource(id = R.drawable.stream_video_ic_options), - contentDescription = stringResource(R.string.stream_video_call_controls_settings), - ) + onCallAction(Settings(!isShowingSettings)) +} + +@Preview +@Composable +public fun ToggleSettingsActionPreview() { + io.getstream.video.android.compose.theme.base.VideoTheme { + Column { + Row { + ToggleSettingsAction(isShowingSettings = false) { + } + + ToggleSettingsAction(isShowingSettings = true) { + } + } + } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleCameraAction.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleCameraAction.kt index 71a743eda8..4e31a9a81a 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleCameraAction.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleCameraAction.kt @@ -16,20 +16,20 @@ package io.getstream.video.android.compose.ui.components.call.controls.actions -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material.icons.filled.VideocamOff import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.Preview import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.ui.components.base.styling.StreamFixedSizeButtonStyle import io.getstream.video.android.core.call.state.CallAction import io.getstream.video.android.core.call.state.ToggleCamera -import io.getstream.video.android.ui.common.R /** * A call action button represents toggling a camera. @@ -44,43 +44,42 @@ public fun ToggleCameraAction( modifier: Modifier = Modifier, isCameraEnabled: Boolean, enabled: Boolean = true, - shape: Shape = VideoTheme.shapes.callControlsButton, - enabledColor: Color = VideoTheme.colors.callActionIconEnabledBackground, - disabledColor: Color = VideoTheme.colors.callActionIconDisabledBackground, - enabledIconTint: Color = VideoTheme.colors.callActionIconEnabled, - disabledIconTint: Color = VideoTheme.colors.callActionIconDisabled, + shape: Shape? = null, + enabledColor: Color? = null, + disabledColor: Color? = null, + enabledIconTint: Color? = null, + disabledIconTint: Color? = null, + onStyle: StreamFixedSizeButtonStyle? = null, + offStyle: StreamFixedSizeButtonStyle? = null, onCallAction: (ToggleCamera) -> Unit, +): Unit = ToggleAction( + modifier = modifier, + enabled = enabled, + shape = shape, + enabledColor = enabledColor, + disabledColor = disabledColor, + enabledIconTint = enabledIconTint, + disabledIconTint = disabledIconTint, + isActionActive = isCameraEnabled, + onStyle = onStyle, + offStyle = offStyle, + iconOnOff = Pair(Icons.Default.Videocam, Icons.Default.VideocamOff), ) { - val cameraIcon = painterResource( - id = if (isCameraEnabled) { - R.drawable.stream_video_ic_videocam_on - } else { - R.drawable.stream_video_ic_videocam_off - }, - ) + onCallAction(ToggleCamera(isCameraEnabled.not())) +} + +@Preview +@Composable +public fun ToggleCameraActionPreview() { + io.getstream.video.android.compose.theme.base.VideoTheme { + Column { + Row { + ToggleCameraAction(isCameraEnabled = false) { + } - CallControlActionBackground( - modifier = modifier, - isEnabled = isCameraEnabled, - shape = shape, - enabledColor = enabledColor, - disabledColor = disabledColor, - ) { - Icon( - modifier = Modifier - .padding(13.dp) - .clickable(enabled = enabled) { - onCallAction( - ToggleCamera(isCameraEnabled.not()), - ) - }, - tint = if (isCameraEnabled) { - enabledIconTint - } else { - disabledIconTint - }, - painter = cameraIcon, - contentDescription = stringResource(R.string.stream_video_call_controls_toggle_camera), - ) + ToggleCameraAction(isCameraEnabled = true) { + } + } + } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleMicrophoneAction.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleMicrophoneAction.kt index f1e9356e35..f2a6556af2 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleMicrophoneAction.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleMicrophoneAction.kt @@ -16,20 +16,19 @@ package io.getstream.video.android.compose.ui.components.call.controls.actions -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.MicOff import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import androidx.compose.ui.tooling.preview.Preview +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.call.state.CallAction import io.getstream.video.android.core.call.state.ToggleMicrophone -import io.getstream.video.android.ui.common.R /** * A call action button represents toggling a microphone. @@ -44,46 +43,38 @@ public fun ToggleMicrophoneAction( modifier: Modifier = Modifier, isMicrophoneEnabled: Boolean, enabled: Boolean = true, - shape: Shape = VideoTheme.shapes.callControlsButton, - enabledColor: Color = VideoTheme.colors.callActionIconEnabledBackground, - disabledColor: Color = VideoTheme.colors.callActionIconDisabledBackground, - enabledIconTint: Color = VideoTheme.colors.callActionIconEnabled, - disabledIconTint: Color = VideoTheme.colors.callActionIconDisabled, + shape: Shape? = null, + enabledColor: Color? = null, + disabledColor: Color? = null, + enabledIconTint: Color? = null, + disabledIconTint: Color? = null, onCallAction: (ToggleMicrophone) -> Unit, +): Unit = ToggleAction( + modifier = modifier, + enabled = enabled, + shape = shape, + enabledColor = enabledColor, + disabledColor = disabledColor, + enabledIconTint = enabledIconTint, + disabledIconTint = disabledIconTint, + isActionActive = isMicrophoneEnabled, + iconOnOff = Pair(Icons.Default.Mic, Icons.Default.MicOff), ) { - val microphoneIcon = - painterResource( - id = if (isMicrophoneEnabled) { - R.drawable.stream_video_ic_mic_on - } else { - R.drawable.stream_video_ic_mic_off - }, - ) + onCallAction(ToggleMicrophone(isMicrophoneEnabled.not())) +} + +@Preview +@Composable +public fun ToggleMicrophoneActionPreview() { + VideoTheme { + Column { + Row { + ToggleMicrophoneAction(isMicrophoneEnabled = false) { + } - CallControlActionBackground( - modifier = modifier, - isEnabled = isMicrophoneEnabled, - shape = shape, - enabledColor = enabledColor, - disabledColor = disabledColor, - ) { - Icon( - modifier = Modifier - .padding(13.dp) - .clickable(enabled = enabled) { - onCallAction( - ToggleMicrophone(isMicrophoneEnabled.not()), - ) - }, - tint = if (isMicrophoneEnabled) { - enabledIconTint - } else { - disabledIconTint - }, - painter = microphoneIcon, - contentDescription = stringResource( - R.string.stream_video_call_controls_toggle_microphone, - ), - ) + ToggleMicrophoneAction(isMicrophoneEnabled = true) { + } + } + } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleSpeakerphoneAction.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleSpeakerphoneAction.kt index 92fa021963..a7528604d2 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleSpeakerphoneAction.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/controls/actions/ToggleSpeakerphoneAction.kt @@ -16,20 +16,19 @@ package io.getstream.video.android.compose.ui.components.call.controls.actions -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.VolumeOff +import androidx.compose.material.icons.filled.VolumeUp import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import androidx.compose.ui.tooling.preview.Preview +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.call.state.CallAction import io.getstream.video.android.core.call.state.ToggleSpeakerphone -import io.getstream.video.android.ui.common.R /** * A call action button represents toggling a speakerphone. @@ -44,45 +43,38 @@ public fun ToggleSpeakerphoneAction( modifier: Modifier = Modifier, isSpeakerphoneEnabled: Boolean, enabled: Boolean = true, - shape: Shape = VideoTheme.shapes.callControlsButton, - enabledColor: Color = VideoTheme.colors.callActionIconEnabledBackground, - disabledColor: Color = VideoTheme.colors.callActionIconDisabledBackground, - enabledIconTint: Color = VideoTheme.colors.callActionIconEnabled, - disabledIconTint: Color = VideoTheme.colors.callActionIconDisabled, + shape: Shape? = null, + enabledColor: Color? = null, + disabledColor: Color? = null, + enabledIconTint: Color? = null, + disabledIconTint: Color? = null, onCallAction: (ToggleSpeakerphone) -> Unit, +): Unit = ToggleAction( + modifier = modifier, + enabled = enabled, + shape = shape, + enabledColor = enabledColor, + disabledColor = disabledColor, + enabledIconTint = enabledIconTint, + disabledIconTint = disabledIconTint, + isActionActive = isSpeakerphoneEnabled, + iconOnOff = Pair(Icons.Default.VolumeUp, Icons.Default.VolumeOff), ) { - val cameraIcon = painterResource( - id = if (isSpeakerphoneEnabled) { - R.drawable.stream_video_ic_speaker_on - } else { - R.drawable.stream_video_ic_speaker_off - }, - ) + onCallAction(ToggleSpeakerphone(isSpeakerphoneEnabled.not())) +} + +@Preview +@Composable +public fun ToggleSpeakerphoneActionPreview() { + VideoTheme { + Column { + Row { + ToggleSpeakerphoneAction(isSpeakerphoneEnabled = false) { + } - CallControlActionBackground( - modifier = modifier, - isEnabled = isSpeakerphoneEnabled, - shape = shape, - enabledColor = enabledColor, - disabledColor = disabledColor, - ) { - Icon( - modifier = Modifier - .padding(13.dp) - .clickable(enabled = enabled) { - onCallAction( - ToggleSpeakerphone(isSpeakerphoneEnabled.not()), - ) - }, - tint = if (isSpeakerphoneEnabled) { - enabledIconTint - } else { - disabledIconTint - }, - painter = cameraIcon, - contentDescription = stringResource( - R.string.stream_video_call_controls_toggle_speakerphone, - ), - ) + ToggleSpeakerphoneAction(isSpeakerphoneEnabled = true) { + } + } + } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/diagnostics/CallDiagnosticsContent.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/diagnostics/CallDiagnosticsContent.kt index a830a6d0e8..ff78719654 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/diagnostics/CallDiagnosticsContent.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/diagnostics/CallDiagnosticsContent.kt @@ -66,7 +66,7 @@ public fun CallDiagnosticsContent( modifier = Modifier .fillMaxWidth() .fillMaxHeight() - .background(color = Color(0x80000000)), + .background(color = Color(0x80B9B9B9)), ) { val stats by call.statsReport.collectAsStateWithLifecycle() val configuration = LocalConfiguration.current diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/lobby/CallLobby.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/lobby/CallLobby.kt index cab9d75001..14b0caa8a4 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/lobby/CallLobby.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/lobby/CallLobby.kt @@ -32,8 +32,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag @@ -43,7 +41,7 @@ import androidx.compose.ui.unit.dp import io.getstream.video.android.compose.lifecycle.MediaPiPLifecycle import io.getstream.video.android.compose.permission.VideoPermissionsState import io.getstream.video.android.compose.permission.rememberCallPermissionsState -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.call.controls.ControlActions import io.getstream.video.android.compose.ui.components.call.controls.actions.DefaultOnCallActionHandler @@ -113,19 +111,16 @@ public fun CallLobby( OnDisabledContent(user = user) }, onCallAction: (CallAction) -> Unit = { DefaultOnCallActionHandler.onCallAction(call, it) }, - lobbyControlsContent: @Composable (call: Call) -> Unit = { + lobbyControlsContent: @Composable (modifier: Modifier, call: Call) -> Unit = { modifier, call -> ControlActions( + modifier = modifier, call = call, - backgroundColor = Color.Transparent, - shape = RectangleShape, - elevation = 0.dp, actions = buildDefaultLobbyControlActions( call = call, onCallAction = onCallAction, isCameraEnabled = isCameraEnabled, isMicrophoneEnabled = isMicrophoneEnabled, ), - spaceBy = VideoTheme.dimens.lobbyControlActionsItemSpaceBy, ) }, ) { @@ -137,10 +132,8 @@ public fun CallLobby( Box( modifier = Modifier .fillMaxWidth() - .height(VideoTheme.dimens.lobbyVideoHeight) - .padding(horizontal = 30.dp) - .clip(RoundedCornerShape(12.dp)) - .background(VideoTheme.colors.callLobbyBackground), + .height(280.dp) + .clip(RoundedCornerShape(12.dp)), ) { if (isCameraEnabled) { onRenderedContent.invoke(video) @@ -160,11 +153,7 @@ public fun CallLobby( hasAudio = isMicrophoneEnabled, soundIndicatorContent = { MicrophoneIndicator( - modifier = Modifier - .align(Alignment.CenterVertically) - .padding( - horizontal = VideoTheme.dimens.participantSoundIndicatorPadding, - ), + modifier = Modifier.padding(horizontal = VideoTheme.dimens.spacingM), isMicrophoneEnabled = isMicrophoneEnabled, ) }, @@ -172,9 +161,9 @@ public fun CallLobby( ) } - Spacer(modifier = Modifier.height(VideoTheme.dimens.lobbyControlActionsPadding)) + Spacer(modifier = Modifier.height(VideoTheme.dimens.spacingM)) - lobbyControlsContent.invoke(call) + lobbyControlsContent.invoke(Modifier.align(Alignment.Start), call) } } @@ -196,6 +185,7 @@ private fun OnRenderedContent( VideoRenderer( modifier = Modifier .fillMaxSize() + .background(VideoTheme.colors.baseSheetTertiary) .testTag("on_rendered_content"), call = call, video = video, @@ -208,11 +198,12 @@ private fun OnDisabledContent(user: User) { Box( modifier = Modifier .fillMaxSize() + .background(VideoTheme.colors.baseSheetTertiary) .testTag("on_disabled_content"), ) { UserAvatar( modifier = Modifier - .size(VideoTheme.dimens.callAvatarSize) + .size(VideoTheme.dimens.genericMax) .align(Alignment.Center), userImage = user.image, userName = user.name.ifBlank { user.id }, diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/lobby/LobbyControlsActions.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/lobby/LobbyControlsActions.kt index d579a3204a..ac44c9a914 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/lobby/LobbyControlsActions.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/lobby/LobbyControlsActions.kt @@ -16,14 +16,8 @@ package io.getstream.video.android.compose.ui.components.call.lobby -import android.content.res.Configuration -import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalInspectionMode -import io.getstream.video.android.compose.theme.VideoTheme 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 @@ -50,25 +44,15 @@ public fun buildDefaultLobbyControlActions( call.microphone.isEnabled.value }, ): List<@Composable () -> Unit> { - val orientation = LocalConfiguration.current.orientation - - val modifier = if (orientation == Configuration.ORIENTATION_PORTRAIT) { - Modifier.size(VideoTheme.dimens.controlActionsButtonSize) - } else { - Modifier.size(VideoTheme.dimens.landscapeControlActionsButtonSize) - } - return listOf( { ToggleMicrophoneAction( - modifier = modifier, isMicrophoneEnabled = isMicrophoneEnabled, onCallAction = onCallAction, ) }, { ToggleCameraAction( - modifier = modifier, isCameraEnabled = isCameraEnabled, onCallAction = onCallAction, ) diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/pinning/ParticipantActions.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/pinning/ParticipantActions.kt index 5684009d8a..d8e25c8a8d 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/pinning/ParticipantActions.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/pinning/ParticipantActions.kt @@ -16,58 +16,42 @@ package io.getstream.video.android.compose.ui.components.call.pinning -import android.content.res.Configuration -import androidx.compose.foundation.Canvas 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.BoxScope import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -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.CircleShape import androidx.compose.material.Icon -import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.outlined.MoreHoriz import androidx.compose.material.icons.outlined.PushPin import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable 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.Alignment.Companion.Center -import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInParent import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.ui.window.Popup import androidx.lifecycle.lifecycleScope -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.compose.ui.components.avatar.UserAvatar +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.indicator.GenericIndicator import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState @@ -87,8 +71,7 @@ import org.openapitools.client.models.OwnCapability * @param condition the condition if the action is to be shown or not. * @param action the action (i.e. callable) */ -@Immutable -public data class ParticipantAction( +public class ParticipantAction( public val icon: ImageVector, public val label: String, public val firstToggleAction: Boolean = true, @@ -162,151 +145,101 @@ internal fun BoxScope.ParticipantActions( var showDialog by remember { mutableStateOf(false) } + ParticipantActionsWithoutState(actions, call, participant, modifier, showDialog) { + showDialog = !showDialog + } +} + +@Composable +private fun BoxScope.ParticipantActionsWithoutState( + actions: List, + call: Call, + participant: ParticipantState, + modifier: Modifier = Modifier, + showDialog: Boolean = false, + onClick: () -> Unit = {}, +) { + val buttonPosition = remember { mutableStateOf(Offset.Zero) } + val buttonSize = remember { mutableStateOf(IntSize.Zero) } if (actions.any { it.condition.invoke(call, participant) } ) { GenericIndicator( + backgroundColor = VideoTheme.colors.baseSheetPrimary, + shape = VideoTheme.shapes.circle, modifier = modifier.clickable { - showDialog = !showDialog - }, + onClick() + }.onGloballyPositioned { coordinates -> + buttonPosition.value = coordinates.positionInParent() + buttonSize.value = coordinates.size + }.clip(VideoTheme.shapes.circle), ) { Icon( imageVector = Icons.Outlined.MoreHoriz, contentDescription = "Call actions", - tint = Color.White, + tint = VideoTheme.colors.basePrimary, ) } if (showDialog) { ParticipantActionsDialog( + offset = IntOffset( + x = buttonPosition.value.x.toInt(), + y = (buttonPosition.value.y + buttonSize.value.height).toInt(), + ), call = call, participant = participant, actions = actions, onDismiss = { - showDialog = false + onClick() }, ) } } } -@OptIn(ExperimentalLayoutApi::class) @Composable internal fun BoxScope.ParticipantActionsDialog( call: Call, participant: ParticipantState, actions: List, onDismiss: () -> Unit = {}, + offset: IntOffset, ) { val coroutineScope = LocalLifecycleOwner.current.lifecycleScope - val userName by participant.userNameOrId.collectAsStateWithLifecycle() - val userImage by participant.image.collectAsStateWithLifecycle() - val name = remember { - val nameValue = participant.name.value - nameValue.ifEmpty { - participant.userNameOrId.value - } - } - Dialog(onDismiss) { + Popup( + offset = offset, + onDismissRequest = onDismiss, + ) { Column( Modifier - .background(VideoTheme.colors.appBackground) - .align(Alignment.Center) - .padding(16.dp), + .background(VideoTheme.colors.baseSheetPrimary, shape = VideoTheme.shapes.dialog) + .align(Center) + .width(220.dp), ) { - UserAvatar( - modifier = Modifier - .size(82.dp) - .align(Alignment.CenterHorizontally) - .aspectRatio(1f), - userName = userName, - userImage = userImage, - ) - Text( - modifier = Modifier.fillMaxWidth(), - text = name, - color = VideoTheme.colors.textHighEmphasis, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(16.dp)) - FlowRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, - verticalArrangement = Arrangement.Center, - ) { - actions.forEach { - if (it.condition.invoke(call, participant)) { - val circleColor = - if (it.firstToggleAction) VideoTheme.colors.textHighEmphasis else VideoTheme.colors.primaryAccent - val strokeWidth = if (it.firstToggleAction) 2.dp else 4.dp - Column { - CircleIcon( - icon = it.icon, - modifier = Modifier.align(CenterHorizontally), - tint = circleColor, - circleColor = circleColor, - strokeWidth = strokeWidth, - ) { - it.action.invoke(coroutineScope, call, participant) - } - Spacer(modifier = Modifier.height(8.dp)) - Text( - modifier = Modifier - .align(CenterHorizontally) - .width(80.dp), - textAlign = TextAlign.Center, - text = it.label, - color = VideoTheme.colors.textHighEmphasis, - ) - } + actions.forEach { + if (it.condition(call, participant)) { + StreamToggleButton( + modifier = Modifier.width(220.dp), + toggleState = rememberUpdatedState( + newValue = ToggleableState(!it.firstToggleAction), + ), + onIcon = it.icon, + onText = it.label, + offText = it.label, + onStyle = VideoTheme.styles.buttonStyles.toggleButtonStyleOn(), + offStyle = VideoTheme.styles.buttonStyles.toggleButtonStyleOff(), + ) { _ -> + it.action.invoke(coroutineScope, call, participant) + onDismiss() } } } - Spacer(modifier = Modifier.height(16.dp)) } } } -@Composable -private fun CircleIcon( - icon: ImageVector, - modifier: Modifier, - tint: Color, - circleColor: Color, - strokeWidth: Dp = 2.dp, - onClick: () -> Unit, -) { - val density = LocalDensity.current - Box( - modifier = modifier - .size(48.dp) - .clip(CircleShape) - .background(Color.Transparent) - .clickable { - onClick() - }, - ) { - Canvas(modifier = Modifier.fillMaxSize(), onDraw = { - drawCircleOutline(density, circleColor, strokeWidth) - }) - Icon( - modifier = Modifier.align(Center), - imageVector = icon, - contentDescription = null, - tint = tint, - ) - } -} - -private fun DrawScope.drawCircleOutline(density: Density, color: Color, width: Dp) { - val strokeWidth = with(density) { width.toPx() } - drawCircle( - color = color, - style = androidx.compose.ui.graphics.drawscope.Stroke(strokeWidth), - ) -} - @Preview @Composable private fun ParticipantActionDialogPreview() { @@ -317,22 +250,28 @@ private fun ParticipantActionDialogPreview() { call = previewCall, participant = previewParticipant, actions = pinUnpinActions, + offset = IntOffset( + x = 0, + y = 50, + ), ) } } } -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview @Composable -private fun ParticipantActionDialogPreviewDark() { +private fun ParticipantActionsPreview() { StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) VideoTheme { Box { - ParticipantActionsDialog( + ParticipantActionsWithoutState( + actions = pinUnpinActions, call = previewCall, participant = previewParticipant, - actions = pinUnpinActions, - ) + showDialog = true, + ) { + } } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/FloatingParticipantVideo.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/FloatingParticipantVideo.kt index 952033b76b..77598e998b 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/FloatingParticipantVideo.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/FloatingParticipantVideo.kt @@ -54,7 +54,7 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.avatar.LocalAvatarPreviewProvider import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState @@ -88,7 +88,7 @@ public fun BoxScope.FloatingParticipantVideo( ParticipantVideo( modifier = Modifier .fillMaxSize() - .clip(VideoTheme.shapes.floatingParticipant), + .clip(VideoTheme.shapes.dialog), call = call, participant = participant, style = style, @@ -106,23 +106,25 @@ public fun BoxScope.FloatingParticipantVideo( offsetY = 0f } - val paddingOffset = density.run { VideoTheme.dimens.floatingVideoPadding.toPx() } + val paddingOffset = density.run { VideoTheme.dimens.spacingS.toPx() } val track by participant.videoTrack.collectAsStateWithLifecycle() if (LocalInspectionMode.current) { + val width = VideoTheme.dimens.genericMax * 2 + val height = width * 1.2f Card( elevation = 8.dp, modifier = Modifier .then(modifier) .align(alignment) - .padding(VideoTheme.dimens.floatingVideoPadding) + .padding(VideoTheme.dimens.spacingS) .onGloballyPositioned { videoSize = it.size } .size( - height = VideoTheme.dimens.floatingVideoHeight, - width = VideoTheme.dimens.floatingVideoWidth, + height = height, + width = width, ) - .clip(VideoTheme.shapes.floatingParticipant), + .clip(VideoTheme.shapes.dialog), shape = RoundedCornerShape(16.dp), ) { Image( @@ -145,8 +147,8 @@ public fun BoxScope.FloatingParticipantVideo( modifier = Modifier .align(alignment) .size( - height = VideoTheme.dimens.floatingVideoHeight, - width = VideoTheme.dimens.floatingVideoWidth, + height = VideoTheme.dimens.genericMax * 1.8f, + width = VideoTheme.dimens.genericMax, ) .offset { IntOffset(offset.x.toInt(), offset.y.toInt()) } .pointerInput(parentBounds) { @@ -184,9 +186,9 @@ public fun BoxScope.FloatingParticipantVideo( } } .then(modifier) - .padding(VideoTheme.dimens.floatingVideoPadding) + .padding(VideoTheme.dimens.spacingS) .onGloballyPositioned { videoSize = it.size }, - shape = VideoTheme.shapes.floatingParticipant, + shape = VideoTheme.shapes.dialog, ) { videoRenderer.invoke(participant) } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt index 395f152e13..80d5cd7258 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt @@ -29,9 +29,11 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape @@ -70,14 +72,14 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp 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.avatar.LocalAvatarPreviewProvider import io.getstream.video.android.compose.ui.components.avatar.UserAvatarBackground import io.getstream.video.android.compose.ui.components.call.pinning.ParticipantAction import io.getstream.video.android.compose.ui.components.call.pinning.ParticipantActions import io.getstream.video.android.compose.ui.components.call.pinning.pinUnpinActions -import io.getstream.video.android.compose.ui.components.connection.NetworkQualityIndicator import io.getstream.video.android.compose.ui.components.indicator.GenericIndicator +import io.getstream.video.android.compose.ui.components.indicator.NetworkQualityIndicator import io.getstream.video.android.compose.ui.components.indicator.SoundIndicator import io.getstream.video.android.compose.ui.components.video.VideoRenderer import io.getstream.video.android.core.Call @@ -120,7 +122,7 @@ public fun ParticipantVideo( networkQuality = it, modifier = Modifier .align(BottomEnd) - .height(VideoTheme.dimens.participantLabelHeight), + .height(VideoTheme.dimens.componentHeightM), ) }, videoFallbackContent: @Composable (Call) -> Unit = { @@ -165,22 +167,18 @@ public fun ParticipantVideo( } } - val containerShape = if (style.isScreenSharing) { - RoundedCornerShape(VideoTheme.dimens.screenShareParticipantsRadius) - } else { - VideoTheme.shapes.participantContainerShape - } + val containerShape = VideoTheme.shapes.sheet val containerModifier = if (style.isFocused && participants.size > 1) { modifier.border( border = if (style.isScreenSharing) { BorderStroke( - VideoTheme.dimens.participantScreenSharingFocusedBorderWidth, - VideoTheme.colors.callFocusedBorder, + VideoTheme.dimens.genericXXs, + VideoTheme.colors.brandPrimary, ) } else { BorderStroke( - VideoTheme.dimens.participantFocusedBorderWidth, - VideoTheme.colors.callFocusedBorder, + VideoTheme.dimens.genericXXs, + VideoTheme.colors.brandPrimary, ) }, shape = containerShape, @@ -191,7 +189,7 @@ public fun ParticipantVideo( Box( modifier = containerModifier .clip(containerShape) - .background(VideoTheme.colors.participantContainerBackground), + .background(VideoTheme.colors.baseSheetTertiary), ) { ParticipantVideoRenderer( call = call, @@ -263,7 +261,6 @@ public fun BoxScope.ParticipantLabel( labelPosition: Alignment = BottomStart, soundIndicatorContent: @Composable RowScope.() -> Unit = { val audioEnabled by participant.audioEnabled.collectAsStateWithLifecycle() - val speaking by participant.speaking.collectAsStateWithLifecycle() val audioLevel by if (participant.isLocal) { call.localMicrophoneAudioLevel.collectAsStateWithLifecycle() } else { @@ -278,12 +275,14 @@ public fun BoxScope.ParticipantLabel( audioLevel = audioLevel, modifier = Modifier .align(CenterVertically) - .padding(horizontal = VideoTheme.dimens.participantSoundIndicatorPadding), + .padding( + vertical = VideoTheme.dimens.spacingXs, + horizontal = VideoTheme.dimens.spacingS, + ), ) }, ) { val audioEnabled by participant.audioEnabled.collectAsStateWithLifecycle() - val speaking by participant.speaking.collectAsStateWithLifecycle() val pinned by remember { derivedStateOf { call.state.pinnedParticipants.value.contains(participant.sessionId) } } @@ -322,22 +321,22 @@ public fun BoxScope.ParticipantLabel( audioLevel = audioLevel, modifier = Modifier .align(CenterVertically) - .padding(horizontal = VideoTheme.dimens.participantSoundIndicatorPadding), + .padding(horizontal = VideoTheme.dimens.spacingS), ) }, ) { var componentWidth by remember { mutableStateOf(0.dp) } - componentWidth = VideoTheme.dimens.participantLabelTextMaxWidth + componentWidth = VideoTheme.dimens.genericMax // get local density from composable val density = LocalDensity.current Box( modifier = Modifier .align(labelPosition) - .height(VideoTheme.dimens.participantLabelHeight) + .height(VideoTheme.dimens.componentHeightM) .wrapContentWidth() .background( - VideoTheme.colors.participantLabelBackground, - shape = VideoTheme.shapes.participantLabelShape, + VideoTheme.colors.baseSheetQuarternary, + shape = RoundedCornerShape(topEnd = VideoTheme.dimens.roundnessM), ) .onGloballyPositioned { componentWidth = with(density) { @@ -352,23 +351,18 @@ public fun BoxScope.ParticipantLabel( Text( modifier = Modifier .widthIn(max = componentWidth) - .padding(start = VideoTheme.dimens.participantLabelTextPaddingStart) + .padding(start = VideoTheme.dimens.spacingM) .align(CenterVertically), text = nameLabel, - style = VideoTheme.typography.body, - color = Color.White, + style = VideoTheme.typography.bodyS, + color = VideoTheme.colors.basePrimary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) if (isPinned) { - GenericIndicator( - modifier = Modifier.padding( - start = VideoTheme.dimens.participantSoundIndicatorPadding, - top = VideoTheme.dimens.participantSoundIndicatorPadding, - bottom = VideoTheme.dimens.participantSoundIndicatorPadding, - ), - ) { + Spacer(modifier = Modifier.size(VideoTheme.dimens.spacingM)) + GenericIndicator { Icon( imageVector = Icons.Filled.PushPin, contentDescription = "Pin", @@ -416,7 +410,7 @@ private fun BoxScope.DefaultReaction( val size: Dp by animateDpAsState( targetValue = if (currentReaction != null) { - VideoTheme.dimens.reactionSize + VideoTheme.dimens.componentHeightL } else { 0.dp }, @@ -473,9 +467,22 @@ private fun ParticipantLabelPreview() { VideoTheme { Box { ParticipantLabel( - call = previewCall, - participant = previewParticipantsList[1], - BottomStart, + nameLabel = "The name", + isPinned = true, + labelPosition = BottomStart, + hasAudio = true, + isSpeaking = true, + audioLevel = 0f, + soundIndicatorContent = { + SoundIndicator( + isSpeaking = true, + isAudioEnabled = true, + audioLevel = 0.8f, + modifier = Modifier + .align(CenterVertically) + .padding(horizontal = VideoTheme.dimens.spacingS), + ) + }, ) } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsLayout.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsLayout.kt index b37f76d9b3..33386d4de0 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsLayout.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsLayout.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntSize 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 io.getstream.video.android.core.ParticipantState import io.getstream.video.android.mock.StreamPreviewDataUtils diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsRegularGrid.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsRegularGrid.kt index b644e6b58e..9256121570 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsRegularGrid.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantsRegularGrid.kt @@ -22,13 +22,12 @@ import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntSize 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.internal.OrientationVideoRenderer import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState @@ -64,7 +63,7 @@ public fun ParticipantsRegularGrid( }, floatingVideoRenderer: @Composable (BoxScope.(call: Call, IntSize) -> Unit)? = null, ) { - Box(modifier = modifier.background(color = VideoTheme.colors.appBackground)) { + Box(modifier = modifier.background(color = VideoTheme.colors.baseSheetPrimary)) { val roomParticipants by call.state.participants.collectAsStateWithLifecycle() if (roomParticipants.isNotEmpty()) { diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/Common.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/Common.kt index 5cc029c003..acaf32c3ee 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/Common.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/Common.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.Call import kotlinx.coroutines.flow.Flow @@ -93,8 +93,7 @@ internal fun SpotlightContentPortrait( content: @Composable () -> Unit, ) { Column( - modifier = modifier - .padding(VideoTheme.dimens.participantsGridPadding), + modifier = modifier, ) { Box( modifier = Modifier @@ -123,7 +122,7 @@ internal fun SpotlightContentLandscape( ) { Row( modifier = modifier - .padding(VideoTheme.dimens.participantsGridPadding), + .padding(end = VideoTheme.dimens.spacingXs), ) { Box( modifier = Modifier diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeScreenSharingVideoRenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeScreenSharingVideoRenderer.kt index 01d4b32835..99834471f6 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeScreenSharingVideoRenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeScreenSharingVideoRenderer.kt @@ -16,7 +16,6 @@ package io.getstream.video.android.compose.ui.components.call.renderer.internal -import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -30,11 +29,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.ScreenSharingVideoRendererStyle import io.getstream.video.android.compose.ui.components.call.renderer.VideoRendererStyle @@ -86,11 +84,11 @@ internal fun LandscapeScreenSharingVideoRenderer( ) { Box( modifier = Modifier - .padding(VideoTheme.dimens.participantsGridPadding) + .padding(VideoTheme.dimens.spacingXs) .clip(RoundedCornerShape(16.dp)) .fillMaxWidth() .weight(0.65f) - .background(VideoTheme.colors.screenSharingBackground), + .background(VideoTheme.colors.baseSheetSecondary), ) { ScreenShareVideoRenderer( modifier = Modifier.fillMaxSize(), @@ -119,40 +117,6 @@ internal fun LandscapeScreenSharingVideoRenderer( } @Preview( - device = Devices.AUTOMOTIVE_1024p, - widthDp = 1440, - heightDp = 720, -) -@Preview( - uiMode = Configuration.UI_MODE_NIGHT_YES, - device = Devices.AUTOMOTIVE_1024p, - widthDp = 1440, - heightDp = 720, -) -@Composable -private fun LandscapeScreenSharingContentPreview() { - StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) - VideoTheme { - LandscapeScreenSharingVideoRenderer( - call = previewCall, - session = ScreenSharingSession( - participant = previewParticipantsList[1], - ), - participants = previewParticipantsList, - dominantSpeaker = previewParticipantsList[1], - modifier = Modifier.fillMaxSize(), - ) - } -} - -@Preview( - device = Devices.AUTOMOTIVE_1024p, - widthDp = 1440, - heightDp = 720, -) -@Preview( - uiMode = Configuration.UI_MODE_NIGHT_YES, - device = Devices.AUTOMOTIVE_1024p, widthDp = 1440, heightDp = 720, ) diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeVideoRenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeVideoRenderer.kt index b5161c6466..968fa6e8b9 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeVideoRenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LandscapeVideoRenderer.kt @@ -40,7 +40,7 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntSize 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.DefaultFloatingParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.RegularVideoRendererStyle @@ -88,7 +88,7 @@ internal fun BoxScope.LandscapeVideoRenderer( floatingVideoRenderer: @Composable (BoxScope.(call: Call, IntSize) -> Unit)? = null, ) { val remoteParticipants by call.state.remoteParticipants.collectAsStateWithLifecycle() - val paddedModifier = modifier.padding(VideoTheme.dimens.participantsGridPadding) + val paddedModifier = modifier.padding(VideoTheme.dimens.spacingXXs) when (callParticipants.size) { 1, 2 -> { val participant = if (remoteParticipants.isEmpty()) { @@ -247,7 +247,7 @@ private fun LandscapeParticipantsPreview1() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { LandscapeVideoRenderer( call = previewCall, @@ -271,7 +271,7 @@ private fun LandscapeParticipantsPreview2() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { LandscapeVideoRenderer( call = previewCall, @@ -295,7 +295,7 @@ private fun LandscapeParticipantsPreview3() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { LandscapeVideoRenderer( call = previewCall, @@ -319,7 +319,7 @@ private fun LandscapeParticipantsPreview4() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { LandscapeVideoRenderer( call = previewCall, @@ -343,7 +343,7 @@ private fun LandscapeParticipantsPreview5() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { LandscapeVideoRenderer( call = previewCall, @@ -367,7 +367,7 @@ private fun LandscapeParticipantsPreview6() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { LandscapeVideoRenderer( call = previewCall, @@ -391,7 +391,7 @@ private fun LandscapeParticipantsPreview7() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { LandscapeVideoRenderer( call = previewCall, diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyColumnVideoRenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyColumnVideoRenderer.kt index fd9fbacf6a..4aa3e48ee1 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyColumnVideoRenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyColumnVideoRenderer.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview -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.ParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.ScreenSharingVideoRendererStyle import io.getstream.video.android.compose.ui.components.call.renderer.VideoRendererStyle @@ -51,8 +51,8 @@ internal fun LazyColumnVideoRenderer( modifier: Modifier = Modifier, state: LazyListState = rememberLazyListState(), itemModifier: Modifier = Modifier.size( - VideoTheme.dimens.screenShareParticipantItemSize * 1.4f, - VideoTheme.dimens.screenShareParticipantItemSize, + VideoTheme.dimens.genericMax * 1.8f, + VideoTheme.dimens.genericMax, ), call: Call, participants: List, @@ -76,7 +76,7 @@ internal fun LazyColumnVideoRenderer( modifier = modifier, state = state, verticalArrangement = Arrangement.spacedBy( - VideoTheme.dimens.screenShareParticipantsListItemMargin, + VideoTheme.dimens.spacingXs, ), horizontalAlignment = Alignment.CenterHorizontally, content = { @@ -86,7 +86,7 @@ internal fun LazyColumnVideoRenderer( ) { index, participant -> ListVideoRenderer( modifier = itemModifier.topOrBottomPadding( - value = VideoTheme.dimens.participantsGridPadding, + value = VideoTheme.dimens.spacingXs, index = index, first = 0, last = participants.lastIndex, diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyRowVideoRenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyRowVideoRenderer.kt index d87f138328..2a87b9b8e7 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyRowVideoRenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/LazyRowVideoRenderer.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview -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.ParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.ScreenSharingVideoRendererStyle import io.getstream.video.android.compose.ui.components.call.renderer.VideoRendererStyle @@ -51,8 +51,8 @@ import io.getstream.video.android.mock.previewParticipantsList internal fun LazyRowVideoRenderer( modifier: Modifier = Modifier, itemModifier: Modifier = Modifier.size( - VideoTheme.dimens.screenShareParticipantItemSize * 1.5f, - VideoTheme.dimens.screenShareParticipantItemSize, + VideoTheme.dimens.genericMax * 1.8f, + VideoTheme.dimens.genericMax, ), call: Call, participants: List, @@ -77,14 +77,14 @@ internal fun LazyRowVideoRenderer( state = state, modifier = modifier, horizontalArrangement = Arrangement.spacedBy( - VideoTheme.dimens.screenShareParticipantsListItemMargin, + VideoTheme.dimens.spacingXs, ), verticalAlignment = Alignment.CenterVertically, ) { itemsIndexed(items = participants, key = { _, it -> it.sessionId }) { index, participant -> ListVideoRenderer( modifier = itemModifier.startOrEndPadding( - value = VideoTheme.dimens.participantsGridPadding, + value = VideoTheme.dimens.spacingXs, index = index, first = 0, last = participants.lastIndex, diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt index 7a292e4654..5e834d6a19 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitScreenSharingVideoRenderer.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.ScreenSharingVideoRendererStyle import io.getstream.video.android.compose.ui.components.call.renderer.VideoRendererStyle @@ -93,7 +93,7 @@ internal fun PortraitScreenSharingVideoRenderer( val me by call.state.me.collectAsStateWithLifecycle() var parentSize: IntSize by remember { mutableStateOf(IntSize(0, 0)) } - val paddedModifier = modifier.padding(VideoTheme.dimens.participantsGridPadding) + val paddedModifier = modifier.padding(VideoTheme.dimens.spacingXXs) BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { LazyVerticalGrid( modifier = Modifier.fillMaxSize(), @@ -145,12 +145,12 @@ private fun BoxWithConstraintsScope.ScreenSharingContent( } Column( modifier = modifier - .padding(VideoTheme.dimens.participantsGridPadding), + .padding(VideoTheme.dimens.spacingXXs), ) { Box( modifier = Modifier .clip(RoundedCornerShape(16.dp)) - .background(VideoTheme.colors.screenSharingBackground) + .background(VideoTheme.colors.baseSheetSecondary) .fillMaxWidth() .height(itemHeight), ) { diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt index a9aaf33bb6..d72005edbc 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntSize 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.DefaultFloatingParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.RegularVideoRendererStyle @@ -93,7 +93,7 @@ internal fun BoxScope.PortraitVideoRenderer( return } - val paddedModifier = modifier.padding(VideoTheme.dimens.participantsGridPadding) + val paddedModifier = modifier.padding(VideoTheme.dimens.spacingXXs) when (callParticipants.size) { 1, 2 -> { val participant = if (remoteParticipants.isEmpty()) { @@ -248,7 +248,7 @@ private fun PortraitParticipantsPreview1() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { PortraitVideoRenderer( call = previewCall, @@ -272,7 +272,7 @@ private fun PortraitParticipantsPreview2() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { PortraitVideoRenderer( call = previewCall, @@ -296,7 +296,7 @@ private fun PortraitParticipantsPreview3() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { PortraitVideoRenderer( call = previewCall, @@ -320,7 +320,7 @@ private fun PortraitParticipantsPreview4() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { PortraitVideoRenderer( call = previewCall, @@ -344,7 +344,7 @@ private fun PortraitParticipantsPreview5() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { PortraitVideoRenderer( call = previewCall, @@ -368,7 +368,7 @@ private fun PortraitParticipantsPreview6() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { PortraitVideoRenderer( call = previewCall, @@ -392,7 +392,7 @@ private fun PortraitParticipantsPreview7() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { PortraitVideoRenderer( call = previewCall, diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/ScreenShareTooltip.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/ScreenShareTooltip.kt index 1b080c3554..6eebeb6b56 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/ScreenShareTooltip.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/ScreenShareTooltip.kt @@ -29,13 +29,17 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.ParticipantState +import io.getstream.video.android.mock.StreamPreviewDataUtils +import io.getstream.video.android.mock.previewParticipant import io.getstream.video.android.ui.common.R @Composable @@ -47,34 +51,43 @@ internal fun ScreenShareTooltip( Row( modifier = modifier - .height(VideoTheme.dimens.screenSharePresenterTooltipHeight) + .height(VideoTheme.dimens.componentHeightM) .wrapContentWidth() .clip(RoundedCornerShape(bottomEnd = 8.dp)) .background( - color = VideoTheme.colors.screenSharingTooltipBackground, + color = VideoTheme.colors.baseSheetQuarternary, shape = RoundedCornerShape(bottomEnd = 8.dp), ), verticalAlignment = Alignment.CenterVertically, ) { Icon( modifier = Modifier.padding( - start = VideoTheme.dimens.screenSharePresenterTooltipPadding, - end = VideoTheme.dimens.screenSharePresenterTooltipIconPadding, + start = VideoTheme.dimens.spacingXs, + end = VideoTheme.dimens.spacingXs, ), painter = painterResource(id = R.drawable.stream_video_ic_screensharing), - tint = VideoTheme.colors.screenSharingTooltipContent, + tint = VideoTheme.colors.basePrimary, contentDescription = "Presenting", ) Text( modifier = Modifier.padding( - end = VideoTheme.dimens.screenSharePresenterTooltipPadding, + end = VideoTheme.dimens.spacingXs, ), text = stringResource(id = R.string.stream_video_screen_sharing_title, userNameOrId), - color = VideoTheme.colors.screenSharingTooltipContent, - style = VideoTheme.typography.title3Bold, + color = VideoTheme.colors.basePrimary, + style = VideoTheme.typography.titleXs, maxLines = 1, overflow = TextOverflow.Ellipsis, ) } } + +@Preview +@Composable +private fun ScreenShareTooltipPreview() { + StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + ScreenShareTooltip(sharingParticipant = previewParticipant) + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/ScreenShareVideoRenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/ScreenShareVideoRenderer.kt index 01f487901c..6c73c2b572 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/ScreenShareVideoRenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/ScreenShareVideoRenderer.kt @@ -22,13 +22,19 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantLabel -import io.getstream.video.android.compose.ui.components.connection.NetworkQualityIndicator +import io.getstream.video.android.compose.ui.components.indicator.NetworkQualityIndicator import io.getstream.video.android.compose.ui.components.video.VideoRenderer import io.getstream.video.android.compose.ui.components.video.VideoScalingType import io.getstream.video.android.core.Call import io.getstream.video.android.core.model.ScreenSharingSession +import io.getstream.video.android.mock.StreamPreviewDataUtils +import io.getstream.video.android.mock.previewCall +import io.getstream.video.android.mock.previewParticipantsList import me.saket.telephoto.zoomable.rememberZoomableState import me.saket.telephoto.zoomable.zoomable @@ -78,3 +84,17 @@ public fun ScreenShareVideoRenderer( } } } + +@Preview +@Composable +private fun ScreenShareVideoRendererPreview() { + VideoTheme { + StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) + ScreenShareVideoRenderer( + call = previewCall, + session = ScreenSharingSession( + participant = previewParticipantsList[0], + ), + ) + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/SpotlightVideorenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/SpotlightVideorenderer.kt index c591d45b82..680393fef1 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/SpotlightVideorenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/SpotlightVideorenderer.kt @@ -43,7 +43,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.SpotlightVideoRendererStyle import io.getstream.video.android.compose.ui.components.call.renderer.VideoRendererStyle @@ -83,7 +83,7 @@ internal fun SpotlightVideoRenderer( if (participants.size == 1) { // Just display the one participant videoRenderer.invoke( - modifier.fillMaxSize().padding(VideoTheme.dimens.participantsGridPadding), + modifier.fillMaxSize().padding(VideoTheme.dimens.spacingS), call, participants[0], style, @@ -106,7 +106,7 @@ internal fun SpotlightVideoRenderer( Row { SpotlightContentLandscape( modifier = modifier.weight(0.7f), - background = VideoTheme.colors.participantContainerBackground, + background = VideoTheme.colors.baseSheetSecondary, ) { SpeakerSpotlight(speaker, videoRenderer, isZoomable, call, style) } @@ -126,11 +126,11 @@ internal fun SpotlightVideoRenderer( } else { // *2 to account for the controls Column( - modifier = Modifier.padding(bottom = VideoTheme.dimens.participantsGridPadding * 2), + modifier = Modifier.padding(bottom = VideoTheme.dimens.spacingXXs * 2), ) { SpotlightContentPortrait( modifier = modifier.weight(1f), - background = VideoTheme.colors.participantContainerBackground, + background = VideoTheme.colors.baseSheetSecondary, ) { SpeakerSpotlight( speaker = speaker, @@ -146,7 +146,7 @@ internal fun SpotlightVideoRenderer( modifier = Modifier .wrapContentWidth() .align(CenterHorizontally) - .height(VideoTheme.dimens.screenShareParticipantItemSize), + .height(VideoTheme.dimens.genericMax), call = call, participants = derivedParticipants, dominantSpeaker = speaker, @@ -189,8 +189,8 @@ private fun Modifier.fillWidthIfParticipantCount(fillCount: Int, totalCount: Int when (totalCount) { fillCount -> this.fillMaxHeight().width(itemWidth.dp) else -> this.size( - VideoTheme.dimens.screenShareParticipantItemSize * 1.5f, - VideoTheme.dimens.screenShareParticipantItemSize, + VideoTheme.dimens.genericMax * 1.5f, + VideoTheme.dimens.genericMax, ) } } @@ -202,12 +202,12 @@ private fun Modifier.fillHeightIfParticipantsCount( val itemHeight = LocalConfiguration.current.screenHeightDp / max(fillCount - 1, 1) when (totalCount) { fillCount -> this.size( - VideoTheme.dimens.screenShareParticipantItemSize * 1.5f, + VideoTheme.dimens.genericMax * 1.5f, itemHeight.dp, ) else -> this.size( - VideoTheme.dimens.screenShareParticipantItemSize * 1.5f, - VideoTheme.dimens.screenShareParticipantItemSize, + VideoTheme.dimens.genericMax * 1.5f, + VideoTheme.dimens.genericMax, ) } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/RingingCallContent.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/RingingCallContent.kt index 8abfb8fbe4..b311572a03 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/RingingCallContent.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/RingingCallContent.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.call.activecall.CallContent import io.getstream.video.android.compose.ui.components.call.controls.actions.DefaultOnCallActionHandler import io.getstream.video.android.compose.ui.components.call.ringing.incomingcall.IncomingCallContent @@ -121,6 +121,10 @@ public fun RingingCallContent( RingingState.Idle -> { // Call state is not ready yet? Show loading? } + + else -> { + // Unknown + } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/incomingcall/IncomingCallContent.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/incomingcall/IncomingCallContent.kt index bbc0c9c5cc..d3104b27a4 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/incomingcall/IncomingCallContent.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/incomingcall/IncomingCallContent.kt @@ -32,10 +32,9 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.avatar.LocalAvatarPreviewPlaceholder import io.getstream.video.android.compose.ui.components.background.CallBackground -import io.getstream.video.android.compose.ui.components.call.CallAppBar import io.getstream.video.android.core.Call import io.getstream.video.android.core.MemberState import io.getstream.video.android.core.call.state.CallAction @@ -128,24 +127,17 @@ public fun IncomingCallContent( onCallAction: (CallAction) -> Unit = {}, ) { CallBackground( - modifier = modifier, - participants = participants, - isVideoType = isVideoType, - isIncoming = true, + modifier = modifier.padding(VideoTheme.dimens.spacingXl), ) { Column { if (isShowingHeader) { - headerContent?.invoke(this) ?: CallAppBar( - call = call, - onBackPressed = onBackPressed, - onCallAction = onCallAction, - ) + headerContent?.invoke(this) } val topPadding = if (participants.size == 1) { - VideoTheme.dimens.singleAvatarAppbarPadding + VideoTheme.dimens.spacingL } else { - VideoTheme.dimens.avatarAppbarPadding + VideoTheme.dimens.spacingM } detailsContent?.invoke(this, participants, topPadding) ?: IncomingCallDetails( modifier = Modifier @@ -159,7 +151,7 @@ public fun IncomingCallContent( controlsContent?.invoke(this) ?: IncomingCallControls( modifier = Modifier .align(Alignment.BottomCenter) - .padding(bottom = VideoTheme.dimens.incomingCallOptionsBottomPadding), + .padding(bottom = VideoTheme.dimens.componentPaddingBottom), isVideoCall = isVideoType, isCameraEnabled = isCameraEnabled, onCallAction = onCallAction, diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/incomingcall/IncomingCallControls.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/incomingcall/IncomingCallControls.kt index 09920031f5..753d342242 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/incomingcall/IncomingCallControls.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/incomingcall/IncomingCallControls.kt @@ -16,20 +16,21 @@ package io.getstream.video.android.compose.ui.components.call.ringing.incomingcall -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +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.size import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import io.getstream.video.android.compose.theme.VideoTheme +import androidx.compose.ui.unit.dp +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.call.controls.actions.AcceptCallAction import io.getstream.video.android.compose.ui.components.call.controls.actions.DeclineCallAction import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleCameraAction -import io.getstream.video.android.compose.ui.extensions.toggleAlpha import io.getstream.video.android.core.call.state.CallAction /** @@ -53,26 +54,19 @@ public fun IncomingCallControls( horizontalArrangement = Arrangement.SpaceEvenly, ) { DeclineCallAction( - modifier = Modifier.size(VideoTheme.dimens.largeButtonSize), onCallAction = onCallAction, ) if (isVideoCall) { ToggleCameraAction( - modifier = Modifier - .toggleAlpha(isCameraEnabled) - .background( - color = VideoTheme.colors.appBackground, - shape = VideoTheme.shapes.callButton, - ) - .size(VideoTheme.dimens.mediumButtonSize), + onStyle = VideoTheme.styles.buttonStyles.tetriaryIconButtonStyle(), + offStyle = VideoTheme.styles.buttonStyles.secondaryIconButtonStyle(), isCameraEnabled = isCameraEnabled, onCallAction = onCallAction, ) } AcceptCallAction( - modifier = Modifier.size(VideoTheme.dimens.largeButtonSize), onCallAction = onCallAction, ) } @@ -82,10 +76,18 @@ public fun IncomingCallControls( @Composable private fun IncomingCallOptionsPreview() { VideoTheme { - IncomingCallControls( - isVideoCall = true, - isCameraEnabled = true, - onCallAction = { }, - ) + Column { + IncomingCallControls( + isVideoCall = true, + isCameraEnabled = true, + onCallAction = { }, + ) + Spacer(modifier = Modifier.size(16.dp)) + IncomingCallControls( + isVideoCall = true, + isCameraEnabled = false, + onCallAction = { }, + ) + } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/incomingcall/IncomingCallDetails.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/incomingcall/IncomingCallDetails.kt index 5c5c08ba28..aacb9a35ba 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/incomingcall/IncomingCallDetails.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/incomingcall/IncomingCallDetails.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview -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.participants.internal.ParticipantAvatars import io.getstream.video.android.compose.ui.components.participants.internal.ParticipantInformation import io.getstream.video.android.core.MemberState @@ -48,7 +48,7 @@ public fun IncomingCallDetails( Column(modifier = modifier.fillMaxWidth()) { ParticipantAvatars(participants = participants) - Spacer(modifier = Modifier.height(VideoTheme.dimens.callParticipantsAvatarsMargin)) + Spacer(modifier = Modifier.height(VideoTheme.dimens.spacingM)) ParticipantInformation( isVideoType = isVideoType, diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/outgoingcall/OutgoingCallContent.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/outgoingcall/OutgoingCallContent.kt index a05e37dfc6..31d929fa53 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/outgoingcall/OutgoingCallContent.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/outgoingcall/OutgoingCallContent.kt @@ -31,9 +31,8 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.background.CallBackground -import io.getstream.video.android.compose.ui.components.call.CallAppBar import io.getstream.video.android.core.Call import io.getstream.video.android.core.MemberState import io.getstream.video.android.core.call.state.CallAction @@ -132,23 +131,16 @@ public fun OutgoingCallContent( CallBackground( modifier = modifier, - participants = participants, - isVideoType = isVideoType, - isIncoming = false, ) { Column { if (isShowingHeader) { - headerContent?.invoke(this) ?: CallAppBar( - call = call, - onBackPressed = onBackPressed, - onCallAction = onCallAction, - ) + headerContent?.invoke(this) } val topPadding = if (participants.size == 1 || isVideoType) { - VideoTheme.dimens.singleAvatarAppbarPadding + VideoTheme.dimens.spacingL } else { - VideoTheme.dimens.avatarAppbarPadding + VideoTheme.dimens.spacingM } detailsContent?.invoke(this, participants, topPadding) ?: OutgoingCallDetails( @@ -163,7 +155,7 @@ public fun OutgoingCallContent( controlsContent?.invoke(this) ?: OutgoingCallControls( modifier = Modifier .align(Alignment.BottomCenter) - .padding(bottom = VideoTheme.dimens.outgoingCallOptionsBottomPadding), + .padding(bottom = VideoTheme.dimens.componentHeightM), isCameraEnabled = isCameraEnabled, isMicrophoneEnabled = isMicrophoneEnabled, onCallAction = onCallAction, diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/outgoingcall/OutgoingCallControls.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/outgoingcall/OutgoingCallControls.kt index b58112fce8..1456939edc 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/outgoingcall/OutgoingCallControls.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/outgoingcall/OutgoingCallControls.kt @@ -16,24 +16,21 @@ package io.getstream.video.android.compose.ui.components.call.ringing.outgoingcall -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement 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.height -import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -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.controls.actions.CancelCallAction 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.extensions.toggleAlpha import io.getstream.video.android.core.call.state.CallAction /** @@ -61,25 +58,13 @@ public fun OutgoingCallControls( horizontalArrangement = Arrangement.SpaceEvenly, ) { ToggleMicrophoneAction( - modifier = Modifier - .toggleAlpha(isMicrophoneEnabled) - .background( - color = VideoTheme.colors.appBackground, - shape = VideoTheme.shapes.callButton, - ) - .size(VideoTheme.dimens.mediumButtonSize), isMicrophoneEnabled = isMicrophoneEnabled, onCallAction = onCallAction, ) ToggleCameraAction( - modifier = Modifier - .toggleAlpha(isCameraEnabled) - .background( - color = VideoTheme.colors.appBackground, - shape = VideoTheme.shapes.callButton, - ) - .size(VideoTheme.dimens.mediumButtonSize), + offStyle = VideoTheme.styles.buttonStyles.secondaryIconButtonStyle(), + onStyle = VideoTheme.styles.buttonStyles.tetriaryIconButtonStyle(), isCameraEnabled = isCameraEnabled, onCallAction = onCallAction, ) @@ -88,7 +73,6 @@ public fun OutgoingCallControls( Spacer(modifier = Modifier.height(32.dp)) CancelCallAction( - modifier = Modifier.size(VideoTheme.dimens.largeButtonSize), onCallAction = onCallAction, ) } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/outgoingcall/OutgoingCallDetails.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/outgoingcall/OutgoingCallDetails.kt index cc037bb397..bdedc14cc7 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/outgoingcall/OutgoingCallDetails.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/outgoingcall/OutgoingCallDetails.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.participants.internal.ParticipantAvatars import io.getstream.video.android.compose.ui.components.participants.internal.ParticipantInformation import io.getstream.video.android.core.MemberState diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/NetworkQualityIndicator.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/NetworkQualityIndicator.kt deleted file mode 100644 index b16cfcf2cf..0000000000 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/NetworkQualityIndicator.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.compose.ui.components.connection - -import androidx.compose.foundation.layout.Row -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.compose.ui.components.connection.internal.ConnectionBars -import io.getstream.video.android.compose.ui.components.connection.internal.barColorsFromQuality -import io.getstream.video.android.compose.ui.components.indicator.GenericIndicator -import io.getstream.video.android.core.model.NetworkQuality -import stream.video.sfu.models.ConnectionQuality - -/** - * Shows the quality of the user's connection depending on the [ConnectionQuality] level. - * - * @param networkQuality The quality level. - * @param modifier Modifier for styling. - */ -@Composable -public fun NetworkQualityIndicator( - networkQuality: NetworkQuality, - modifier: Modifier = Modifier, -) { - val colors = barColorsFromQuality(networkQuality) - GenericIndicator( - modifier = modifier, - shape = VideoTheme.shapes.connectionQualityIndicator, - backgroundColor = VideoTheme.colors.connectionQualityBackground, - ) { - ConnectionBars(colors = colors) - } -} - -@Preview -@Composable -private fun ConnectionQualityIndicatorPreview() { - VideoTheme { - Row { - NetworkQualityIndicator( - networkQuality = NetworkQuality.UnSpecified(), - ) - NetworkQualityIndicator( - networkQuality = NetworkQuality.Poor(), - ) - NetworkQualityIndicator( - networkQuality = NetworkQuality.Good(), - ) - NetworkQualityIndicator( - networkQuality = NetworkQuality.Excellent(), - ) - } - } -} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/internal/ConnectionBars.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/internal/ConnectionBars.kt deleted file mode 100644 index 47dc858e23..0000000000 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/connection/internal/ConnectionBars.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.compose.ui.components.connection.internal - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.core.model.NetworkQuality - -@Composable -internal fun barColorsFromQuality( - networkQuality: NetworkQuality, -): Triple = when (networkQuality) { - is NetworkQuality.Excellent -> Triple( - VideoTheme.colors.connectionQualityBarFilled, - VideoTheme.colors.connectionQualityBarFilled, - VideoTheme.colors.connectionQualityBarFilled, - ) - is NetworkQuality.Good -> Triple( - VideoTheme.colors.connectionQualityBarFilled, - VideoTheme.colors.connectionQualityBarFilled, - VideoTheme.colors.connectionQualityBar, - ) - is NetworkQuality.Poor -> Triple( - VideoTheme.colors.connectionQualityBarFilledPoor, - VideoTheme.colors.connectionQualityBar, - VideoTheme.colors.connectionQualityBar, - ) - is NetworkQuality.UnSpecified -> Triple( - VideoTheme.colors.connectionQualityBar, - VideoTheme.colors.connectionQualityBar, - VideoTheme.colors.connectionQualityBar, - ) -} - -@Composable -internal fun ConnectionBars(modifier: Modifier = Modifier, colors: Triple) { - Row( - modifier = modifier - .padding(VideoTheme.dimens.connectionIndicatorBarWidth) - .height(height = VideoTheme.dimens.connectionIndicatorBarMaxHeight), - verticalAlignment = Alignment.Bottom, - ) { - Spacer( - modifier = Modifier - .width(VideoTheme.dimens.connectionIndicatorBarWidth) - .fillMaxHeight(0.4f) - .background( - color = colors.first, - shape = VideoTheme.shapes.connectionIndicatorBar, - ), - ) - Spacer(modifier = Modifier.width(VideoTheme.dimens.connectionIndicatorBarSeparatorWidth)) - Spacer( - modifier = Modifier - .width(VideoTheme.dimens.connectionIndicatorBarWidth) - .fillMaxHeight(fraction = 0.7f) - .background( - color = colors.second, - shape = VideoTheme.shapes.connectionIndicatorBar, - ), - ) - Spacer(modifier = Modifier.width(VideoTheme.dimens.connectionIndicatorBarSeparatorWidth)) - Spacer( - modifier = Modifier - .width(VideoTheme.dimens.connectionIndicatorBarWidth) - .fillMaxHeight(fraction = 1f) - .background( - color = colors.third, - shape = VideoTheme.shapes.connectionIndicatorBar, - ), - ) - } -} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/AudioVolumeIndicator.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/AudioVolumeIndicator.kt index 2d361ca50e..7d6b988808 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/AudioVolumeIndicator.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/AudioVolumeIndicator.kt @@ -29,9 +29,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme /** * Used to indicate the active sound levels of a given participant. @@ -43,23 +43,19 @@ import io.getstream.video.android.compose.theme.VideoTheme public fun AudioVolumeIndicator( modifier: Modifier = Modifier, audioLevels: Float, + color: Color = VideoTheme.colors.brandPrimary, ) { - val activatedColor = VideoTheme.colors.activatedVolumeIndicator - val deActivatedColor = VideoTheme.colors.deActivatedVolumeIndicator - - val defaultBarHeight = 0.23f - + val defaultBarHeight = 0.1f Row( modifier = modifier - .height(height = VideoTheme.dimens.audioLevelIndicatorBarMaxHeight) - .padding(horizontal = 2.dp), + .height(height = VideoTheme.dimens.componentHeightS) + .padding(horizontal = VideoTheme.dimens.spacingXXs), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy( - VideoTheme.dimens.audioLevelIndicatorBarSeparatorWidth, + VideoTheme.dimens.spacingXXs, ), ) { repeat(3) { index -> - // First bar 60%, second 100%, third 33% val audioLevel = when (index) { @@ -75,10 +71,9 @@ public fun AudioVolumeIndicator( audioLevels } } - Spacer( modifier = Modifier - .width(VideoTheme.dimens.audioLevelIndicatorBarWidth) + .width(VideoTheme.dimens.spacingXXs) .fillMaxHeight( if (audioLevel == 0f) { defaultBarHeight @@ -87,8 +82,8 @@ public fun AudioVolumeIndicator( }, ) .background( - color = if (audioLevel == 0f) deActivatedColor else activatedColor, - shape = RoundedCornerShape(16.dp), + color = color, + shape = RoundedCornerShape(VideoTheme.dimens.roundnessM), ), ) } @@ -101,7 +96,7 @@ private fun ActiveSoundLevelsPreview() { VideoTheme { Column { AudioVolumeIndicator(audioLevels = 0f) - AudioVolumeIndicator(audioLevels = 0.3f) + AudioVolumeIndicator(audioLevels = 0.2f) } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/GenericIndicator.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/GenericIndicator.kt index 2dc6fec105..fd4bc759c3 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/GenericIndicator.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/GenericIndicator.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme /** * A composable that wraps its content into a rounded semi-transparent background. @@ -37,16 +37,14 @@ import io.getstream.video.android.compose.theme.VideoTheme @Composable internal fun GenericIndicator( modifier: Modifier = Modifier, - shape: Shape = VideoTheme.shapes.indicatorBackground, - backgroundColor: Color = VideoTheme.colors.audioIndicatorBackground, + shape: Shape = VideoTheme.shapes.indicator, + backgroundColor: Color = VideoTheme.colors.baseSheetQuarternary, content: @Composable BoxScope.() -> Unit, ) { + // val alphaColor = backgroundColor.copy(alpha = 0.3f) Box( - modifier = modifier.size(VideoTheme.dimens.indicatorBackgroundSize), + modifier = modifier.size(VideoTheme.dimens.componentHeightM), ) { - val backgroundModifier = modifier - .matchParentSize() - // Ensure content is center aligned and padded Box( modifier = Modifier @@ -73,22 +71,13 @@ internal fun GenericIndicator( private fun PreviewIndicatorBackground() { VideoTheme { Column { - GenericIndicator( - backgroundColor = VideoTheme.colors.audioIndicatorBackground, - shape = VideoTheme.shapes.indicatorBackground, - ) { + GenericIndicator { AudioVolumeIndicator(audioLevels = 0.5f) } - GenericIndicator( - backgroundColor = VideoTheme.colors.audioIndicatorBackground, - shape = VideoTheme.shapes.indicatorBackground, - ) { + GenericIndicator { MicrophoneIndicator(isMicrophoneEnabled = false) } - GenericIndicator( - backgroundColor = VideoTheme.colors.audioIndicatorBackground, - shape = VideoTheme.shapes.indicatorBackground, - ) { + GenericIndicator { MicrophoneIndicator(isMicrophoneEnabled = true) } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/MicrophoneIndicator.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/MicrophoneIndicator.kt index 5508ed5ff4..7a234d17c2 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/MicrophoneIndicator.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/MicrophoneIndicator.kt @@ -18,17 +18,16 @@ package io.getstream.video.android.compose.ui.components.indicator import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.MicOff import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.ui.common.R +import io.getstream.video.android.compose.theme.base.VideoTheme /** * Used to indicate the microphone state of a given participant. @@ -43,22 +42,21 @@ public fun MicrophoneIndicator( ) { Box( modifier = modifier - .size(VideoTheme.dimens.microphoneIndicatorSize) - .padding(VideoTheme.dimens.microphoneIndicatorPadding), + .size(VideoTheme.dimens.genericM), ) { if (isMicrophoneEnabled) { Icon( modifier = Modifier.align(Alignment.Center), - painter = painterResource(id = R.drawable.stream_video_ic_mic_on), - tint = Color.White, - contentDescription = "microphone enabled", + imageVector = Icons.Default.Mic, + tint = VideoTheme.colors.basePrimary, + contentDescription = Icons.Default.Mic.name, ) } else { Icon( modifier = Modifier.align(Alignment.Center), - painter = painterResource(id = R.drawable.stream_video_ic_mic_off), - tint = Color.White, - contentDescription = "microphone disabled", + imageVector = Icons.Default.MicOff, + tint = VideoTheme.colors.basePrimary, + contentDescription = Icons.Default.MicOff.name, ) } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/NetworkQualityIndicator.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/NetworkQualityIndicator.kt new file mode 100644 index 0000000000..0eef3f2ac0 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/NetworkQualityIndicator.kt @@ -0,0 +1,140 @@ +/* + * 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.compose.ui.components.indicator + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.core.model.NetworkQuality +import stream.video.sfu.models.ConnectionQuality + +/** + * Shows the quality of the user's connection depending on the [ConnectionQuality] level. + * + * @param networkQuality The quality level. + * @param modifier Modifier for styling. + */ +@Composable +public fun NetworkQualityIndicator( + networkQuality: NetworkQuality, + modifier: Modifier = Modifier, +) { + val colors = barColorsFromQuality(networkQuality) + GenericIndicator( + shape = RoundedCornerShape(topStart = VideoTheme.dimens.roundnessM), + modifier = modifier, + ) { + ConnectionBars(colors = colors) + } +} + +@Composable +internal fun barColorsFromQuality( + networkQuality: NetworkQuality, +): Triple = when (networkQuality) { + is NetworkQuality.Excellent -> Triple( + VideoTheme.colors.brandGreen, + VideoTheme.colors.brandGreen, + VideoTheme.colors.brandGreen, + ) + is NetworkQuality.Good -> Triple( + VideoTheme.colors.brandYellow, + VideoTheme.colors.brandYellow, + VideoTheme.colors.basePrimary, + ) + is NetworkQuality.Poor -> Triple( + VideoTheme.colors.brandRed, + VideoTheme.colors.basePrimary, + VideoTheme.colors.basePrimary, + ) + is NetworkQuality.UnSpecified -> Triple( + VideoTheme.colors.basePrimary, + VideoTheme.colors.basePrimary, + VideoTheme.colors.basePrimary, + ) +} + +@Composable +internal fun ConnectionBars(modifier: Modifier = Modifier, colors: Triple) { + val shape = RoundedCornerShape(VideoTheme.dimens.roundnessM) + Row( + modifier = modifier + .height(height = VideoTheme.dimens.genericS), + verticalAlignment = Alignment.Bottom, + ) { + Spacer( + modifier = Modifier + .width(VideoTheme.dimens.genericXXs) + .fillMaxHeight(0.4f) + .background( + color = colors.first, + shape = shape, + ), + ) + Spacer(modifier = Modifier.width(VideoTheme.dimens.genericXXs)) + Spacer( + modifier = Modifier + .width(VideoTheme.dimens.genericXXs) + .fillMaxHeight(fraction = 0.7f) + .background( + color = colors.second, + shape = shape, + ), + ) + Spacer(modifier = Modifier.width(VideoTheme.dimens.genericXXs)) + Spacer( + modifier = Modifier + .width(VideoTheme.dimens.genericXXs) + .fillMaxHeight(fraction = 1f) + .background( + color = colors.third, + shape = shape, + ), + ) + } +} + +@Preview +@Composable +private fun ConnectionQualityIndicatorPreview() { + VideoTheme { + Row { + NetworkQualityIndicator( + networkQuality = NetworkQuality.UnSpecified(), + ) + NetworkQualityIndicator( + networkQuality = NetworkQuality.Poor(), + ) + NetworkQualityIndicator( + networkQuality = NetworkQuality.Good(), + ) + NetworkQualityIndicator( + networkQuality = NetworkQuality.Excellent(), + ) + } + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/SoundIndicator.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/SoundIndicator.kt index 8509431bfb..788b04f725 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/SoundIndicator.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/indicator/SoundIndicator.kt @@ -21,7 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme /** * Used to indicate the sound state of a given participant. Either shows a mute icon or the sound @@ -39,11 +39,7 @@ public fun SoundIndicator( isAudioEnabled: Boolean, audioLevel: Float, ) { - GenericIndicator( - modifier = modifier, - backgroundColor = VideoTheme.colors.audioIndicatorBackground, - shape = VideoTheme.shapes.indicatorBackground, - ) { + GenericIndicator(modifier = modifier) { if (isAudioEnabled && isSpeaking) { AudioVolumeIndicator( modifier = Modifier.align(Alignment.Center), diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamBackStage.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamBackStage.kt index 26ff7a610c..d1bcae523b 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamBackStage.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamBackStage.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.sp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.ui.common.R @Composable @@ -34,6 +34,6 @@ internal fun BoxScope.LivestreamBackStage() { id = R.string.stream_video_livestreaming_on_backstage, ), fontSize = 14.sp, - color = VideoTheme.colors.textHighEmphasis, + color = VideoTheme.colors.basePrimary, ) } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayer.kt index e8ffa387b2..ba9203f635 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayer.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview 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 io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewCall diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayerOverlay.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayerOverlay.kt index 60bab351ea..e0aff5f7a7 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayerOverlay.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamPlayerOverlay.kt @@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -46,7 +45,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleSpeakerphoneAction import io.getstream.video.android.core.Call import io.getstream.video.android.mock.StreamPreviewDataUtils @@ -81,8 +80,8 @@ private fun BoxScope.LiveBadge(call: Call) { Text( modifier = Modifier .background( - color = VideoTheme.colors.primaryAccent, - shape = RoundedCornerShape(6.dp), + color = VideoTheme.colors.brandPrimary, + shape = VideoTheme.shapes.container, ) .padding(horizontal = 16.dp, vertical = 4.dp), text = stringResource( @@ -125,7 +124,7 @@ private fun BoxScope.LiveDuration(call: Call) { modifier = Modifier .size(8.dp) .clip(CircleShape) - .background(VideoTheme.colors.liveIndicator), + .background(VideoTheme.colors.alertWarning), ) Text( @@ -151,10 +150,6 @@ private fun BoxScope.LiveControls(call: Call) { .align(Alignment.CenterEnd) .size(45.dp), isSpeakerphoneEnabled = speakerphoneEnabled, - enabledColor = VideoTheme.colors.callActionIconEnabledBackground, - disabledColor = VideoTheme.colors.callActionIconEnabledBackground, - disabledIconTint = VideoTheme.colors.errorAccent, - shape = RoundedCornerShape(8.dp), onCallAction = { callAction -> call.speaker.setEnabled(callAction.isEnabled) }, ) } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamRenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamRenderer.kt index 1e17e5cc98..4c948acbbf 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamRenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/livestream/LivestreamRenderer.kt @@ -36,9 +36,14 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.video.VideoRenderer import io.getstream.video.android.core.Call +import io.getstream.video.android.mock.StreamPreviewDataUtils +import io.getstream.video.android.mock.previewCall import io.getstream.webrtc.android.ui.VideoTextureViewRenderer @Composable @@ -91,3 +96,12 @@ internal fun LivestreamRenderer( } } } + +@Preview +@Composable +private fun LiveStreamRendererPreview() { + StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + LivestreamRenderer(call = previewCall, enablePausing = true) + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/CallParticipantsInfoMenu.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/CallParticipantsInfoMenu.kt index a32a776df3..f0226ef626 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/CallParticipantsInfoMenu.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/CallParticipantsInfoMenu.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.getstream.video.android.compose.state.ui.internal.CallParticipantInfoMode import io.getstream.video.android.compose.state.ui.internal.ParticipantListMode -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.participants.internal.CallParticipantsList import io.getstream.video.android.compose.ui.components.participants.internal.InviteUserList import io.getstream.video.android.core.Call @@ -79,13 +79,13 @@ public fun CallParticipantsInfoMenu( Box( modifier = Modifier .fillMaxSize() - .background(color = VideoTheme.colors.infoMenuOverlayColor), + .background(color = VideoTheme.colors.baseSheetPrimary), ) { Column(modifier) { val listModifier = Modifier .weight(2f) .fillMaxWidth() - .background(VideoTheme.colors.appBackground) + .background(VideoTheme.colors.baseSheetPrimary) if (infoStateMode is ParticipantListMode) { CallParticipantsList( diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/ParticipantIndicator.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/ParticipantIndicator.kt deleted file mode 100644 index 7278f1db77..0000000000 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/ParticipantIndicator.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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.compose.ui.components.participants - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.ui.common.R - -/** - * An icon that displays participant number. - * - * @param number Indicates the number of participants. - * @param numberColor A color of the number. - * @param numberBackgroundColor A color of the background of the color badge. - * @param isShowingNumber Whether displays the number or not. - * @param onClick A click callback for the icon. - */ -@Composable -public fun ParticipantIndicatorIcon( - number: Int, - numberColor: Color = Color.White, - numberBackgroundColor: Color = VideoTheme.colors.textLowEmphasis, - isShowingNumber: Boolean = true, - onClick: () -> Unit, -) { - Box { - IconButton( - onClick = { onClick.invoke() }, - modifier = Modifier.padding( - start = VideoTheme.dimens.callAppBarLeadingContentSpacingStart, - end = VideoTheme.dimens.callAppBarLeadingContentSpacingEnd, - ), - ) { - Icon( - painter = painterResource(id = R.drawable.stream_video_ic_participants), - contentDescription = stringResource( - id = R.string.stream_video_call_participants_menu_content_description, - ), - tint = VideoTheme.colors.callDescription, - ) - } - - if (isShowingNumber) { - Text( - modifier = Modifier - .align(Alignment.TopEnd) - .drawBehind { - drawRoundRect( - color = numberBackgroundColor, - cornerRadius = CornerRadius(this.size.maxDimension), - ) - } - .widthIn(min = 21.dp) - .padding(3.dp), - text = number.toString(), - textAlign = TextAlign.Center, - fontSize = 12.sp, - color = numberColor, - maxLines = 1, - ) - } - } -} - -@Preview -@Composable -private fun ParticipantIndicatorIconPreview() { - VideoTheme { - ParticipantIndicatorIcon(number = 10) {} - } -} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/CallParticipantListAppBar.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/CallParticipantListAppBar.kt index a820b7f99d..7bc380fccc 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/CallParticipantListAppBar.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/CallParticipantListAppBar.kt @@ -17,23 +17,20 @@ package io.getstream.video.android.compose.ui.components.participants.internal import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -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.runtime.Composable 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.res.stringResource import androidx.compose.ui.tooling.preview.Preview -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.StreamIconButton import io.getstream.video.android.ui.common.R /** @@ -53,41 +50,29 @@ internal fun CallParticipantListAppBar( Row( modifier = Modifier .fillMaxWidth() - .height(VideoTheme.dimens.participantInfoMenuAppBarHeight) - .background(VideoTheme.colors.barsBackground) - .padding(VideoTheme.dimens.callAppBarPadding), + .background(VideoTheme.colors.baseSheetPrimary), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { Text( modifier = Modifier - .weight(1f) .padding( - start = VideoTheme.dimens.callAppBarCenterContentSpacingStart, - end = VideoTheme.dimens.callAppBarCenterContentSpacingEnd, + start = VideoTheme.dimens.componentPaddingStart, + end = VideoTheme.dimens.componentPaddingEnd, ), text = resources.getQuantityString( R.plurals.stream_video_call_participants_info_number_of_participants, numberOfParticipants, numberOfParticipants, ), - style = VideoTheme.typography.title3, - color = VideoTheme.colors.textHighEmphasis, + style = VideoTheme.typography.titleS, + color = VideoTheme.colors.basePrimary, ) - IconButton( - modifier = Modifier - .fillMaxHeight() - .aspectRatio(1f, matchHeightConstraintsFirst = true), + StreamIconButton( onClick = onBackPressed, - content = { - Icon( - painter = painterResource(id = R.drawable.stream_video_ic_close), - contentDescription = stringResource( - id = R.string.stream_video_back_button_content_description, - ), - tint = VideoTheme.colors.textHighEmphasis, - ) - }, + icon = Icons.Default.Close, + style = VideoTheme.styles.buttonStyles.onlyIconIconButtonStyle(), ) } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/CallParticipantsInfoActions.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/CallParticipantsInfoActions.kt index 3aba859e85..3fb9d5af25 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/CallParticipantsInfoActions.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/CallParticipantsInfoActions.kt @@ -16,23 +16,20 @@ package io.getstream.video.android.compose.ui.components.participants.internal -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.OutlinedButton -import androidx.compose.material.Text +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamButton +import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleMicrophoneAction import io.getstream.video.android.ui.common.R /** @@ -54,49 +51,19 @@ internal fun CallParticipantsInfoActions( Row( modifier = modifier.padding(vertical = 20.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly, + horizontalArrangement = Arrangement.Center, ) { - Button( + StreamButton( modifier = Modifier - .height(VideoTheme.dimens.participantsInfoMenuOptionsButtonHeight) - .weight(1f) - .padding(start = 16.dp, end = 8.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = VideoTheme.colors.primaryAccent), - shape = VideoTheme.shapes.participantsInfoMenuButton, + .weight(1f), onClick = { onInviteUser.invoke() }, - content = { - Text( - text = stringResource( - R.string.stream_video_call_participants_info_options_invite, - ), - style = VideoTheme.typography.bodyBold, - color = Color.White, - ) - }, + text = stringResource(id = R.string.stream_video_call_participants_info_options_invite), + style = VideoTheme.styles.buttonStyles.secondaryButtonStyle(), ) - - OutlinedButton( - modifier = Modifier - .weight(1f) - .height(VideoTheme.dimens.participantsInfoMenuOptionsButtonHeight) - .padding(start = 8.dp, end = 16.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = VideoTheme.colors.appBackground), - border = BorderStroke(1.dp, VideoTheme.colors.textLowEmphasis), - onClick = { onMute(!isLocalAudioEnabled) }, - shape = VideoTheme.shapes.participantsInfoMenuButton, - content = { - Text( - text = stringResource( - if (isLocalAudioEnabled) { - R.string.stream_video_call_participants_info_options_mute - } else { - R.string.stream_video_call_participants_info_options_unmute - }, - ), - style = VideoTheme.typography.bodyBold, - color = VideoTheme.colors.textLowEmphasis, - ) - }, + Spacer(modifier = Modifier.size(VideoTheme.dimens.spacingM)) + ToggleMicrophoneAction( + isMicrophoneEnabled = isLocalAudioEnabled, + onCallAction = { onMute(!isLocalAudioEnabled) }, ) } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/CallParticipantsList.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/CallParticipantsList.kt index dc97fe3c00..8ed83724a8 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/CallParticipantsList.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/CallParticipantsList.kt @@ -43,8 +43,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp 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.avatar.UserAvatar +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize import io.getstream.video.android.core.ParticipantState import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewParticipantsList @@ -69,7 +70,7 @@ internal fun CallParticipantsList( ) { Scaffold( modifier = modifier, - backgroundColor = VideoTheme.colors.appBackground, + backgroundColor = VideoTheme.colors.baseSheetPrimary, topBar = { CallParticipantListAppBar( numberOfParticipants = participants.size, @@ -78,7 +79,7 @@ internal fun CallParticipantsList( }, bottomBar = { CallParticipantsInfoActions( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(horizontal = VideoTheme.dimens.spacingM), isLocalAudioEnabled = isLocalAudioEnabled, onInviteUser = onInviteUser, onMute = onMute, @@ -89,7 +90,7 @@ internal fun CallParticipantsList( modifier = Modifier .fillMaxSize() .padding(padding) - .background(VideoTheme.colors.appBackground), + .background(VideoTheme.colors.baseSheetPrimary), contentPadding = PaddingValues(16.dp), horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(8.dp), @@ -110,7 +111,7 @@ internal fun CallParticipantsList( @Composable private fun CallParticipantInfoItem( participant: ParticipantState, - onUserOptionsSelected: (ParticipantState) -> Unit, + onUserOptionsSelected: ((ParticipantState) -> Unit)? = null, ) { Row( modifier = Modifier.wrapContentWidth(), @@ -122,7 +123,8 @@ private fun CallParticipantInfoItem( val userName by participant.userNameOrId.collectAsStateWithLifecycle() val userImage by participant.image.collectAsStateWithLifecycle() UserAvatar( - modifier = Modifier.size(VideoTheme.dimens.participantsInfoAvatarSize), + textSize = StyleSize.S, + modifier = Modifier.size(VideoTheme.dimens.genericL), userName = userName, userImage = userImage, isShowingOnlineIndicator = true, @@ -133,8 +135,8 @@ private fun CallParticipantInfoItem( .padding(start = 8.dp) .weight(1f), text = userName, - style = VideoTheme.typography.bodyBold, - color = VideoTheme.colors.textHighEmphasis, + style = VideoTheme.typography.bodyM, + color = VideoTheme.colors.basePrimary, fontSize = 16.sp, maxLines = 1, ) @@ -144,7 +146,7 @@ private fun CallParticipantInfoItem( if (!audioEnabled) { Icon( painter = painterResource(id = R.drawable.stream_video_ic_mic_off), - tint = VideoTheme.colors.errorAccent, + tint = VideoTheme.colors.alertWarning, contentDescription = null, ) } @@ -155,21 +157,23 @@ private fun CallParticipantInfoItem( if (!videoEnabled) { Icon( painter = painterResource(id = R.drawable.stream_video_ic_videocam_off), - tint = VideoTheme.colors.errorAccent, + tint = VideoTheme.colors.alertWarning, contentDescription = null, ) } Spacer(modifier = Modifier.width(8.dp)) - Icon( - modifier = Modifier.clickable { onUserOptionsSelected(participant) }, - painter = painterResource(id = R.drawable.stream_video_ic_options), - tint = VideoTheme.colors.textHighEmphasis, - contentDescription = null, - ) + onUserOptionsSelected?.let { + Icon( + modifier = Modifier.clickable { onUserOptionsSelected(participant) }, + painter = painterResource(id = R.drawable.stream_video_ic_options), + tint = VideoTheme.colors.basePrimary, + contentDescription = null, + ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(8.dp)) + } } } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/InviteUserList.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/InviteUserList.kt index ddbbbedd69..e150469f2f 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/InviteUserList.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/InviteUserList.kt @@ -36,8 +36,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.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.ParticipantState import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewParticipantsList @@ -104,8 +105,9 @@ internal fun InviteUserItem( val userImage by user.image.collectAsStateWithLifecycle() UserAvatar( - modifier = Modifier.size(VideoTheme.dimens.participantsInfoAvatarSize), + modifier = Modifier.size(VideoTheme.dimens.componentHeightL), userName = userName, + textSize = StyleSize.S, userImage = userImage, isShowingOnlineIndicator = true, ) @@ -115,8 +117,8 @@ internal fun InviteUserItem( Text( modifier = Modifier.weight(1f), text = userName, - style = VideoTheme.typography.bodyBold, - color = VideoTheme.colors.textHighEmphasis, + style = VideoTheme.typography.bodyM, + color = VideoTheme.colors.basePrimary, ) Spacer(modifier = Modifier.width(8.dp)) diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/ParticipantAvatars.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/ParticipantAvatars.kt index 2fcd059589..128aa241a2 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/ParticipantAvatars.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/ParticipantAvatars.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.compose.ui.components.avatar.UserAvatar import io.getstream.video.android.core.MemberState import io.getstream.video.android.mock.StreamPreviewDataUtils @@ -48,7 +48,7 @@ public fun ParticipantAvatars( val participant = participants.first() UserAvatar( - modifier = Modifier.size(VideoTheme.dimens.singleAvatarSize), + modifier = Modifier.size(VideoTheme.dimens.genericMax), userName = participant.user.userNameOrId, userImage = participant.user.image, ) @@ -57,7 +57,7 @@ public fun ParticipantAvatars( LazyRow(horizontalArrangement = Arrangement.spacedBy(20.dp)) { items(participants.take(2)) { participant -> UserAvatar( - modifier = Modifier.size(VideoTheme.dimens.callAvatarSize), + modifier = Modifier.size(VideoTheme.dimens.genericL), userName = participant.user.userNameOrId, userImage = participant.user.image, ) @@ -66,7 +66,7 @@ public fun ParticipantAvatars( if (participants.size >= 3) { UserAvatar( - modifier = Modifier.size(VideoTheme.dimens.callAvatarSize), + modifier = Modifier.size(VideoTheme.dimens.genericM), userName = participants[2].user.userNameOrId, userImage = participants[2].user.image, ) diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/ParticipantInformation.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/ParticipantInformation.kt index d756f701f9..5ee79d5355 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/ParticipantInformation.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/participants/internal/ParticipantInformation.kt @@ -28,15 +28,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.capitalize -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.MemberState import io.getstream.video.android.core.model.CallStatus import io.getstream.video.android.core.utils.toCallUser @@ -64,20 +62,20 @@ public fun ParticipantInformation( } val fontSize = if (participants.size == 1) { - VideoTheme.dimens.directCallUserNameTextSize + VideoTheme.dimens.textSizeL } else { - VideoTheme.dimens.groupCallUserNameTextSize + VideoTheme.dimens.textSizeM } Text( - modifier = Modifier.padding(horizontal = VideoTheme.dimens.participantsTextPadding), + modifier = Modifier.padding(horizontal = VideoTheme.dimens.spacingM), text = text, fontSize = fontSize, - color = VideoTheme.colors.callDescription, + color = VideoTheme.colors.basePrimary, textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.height(VideoTheme.dimens.callStatusParticipantsMargin)) + Spacer(modifier = Modifier.height(VideoTheme.dimens.spacingM)) val callType = if (isVideoType) { "video" @@ -86,7 +84,6 @@ public fun ParticipantInformation( } Text( - modifier = Modifier.alpha(VideoTheme.dimens.onCallStatusTextAlpha), text = when (callStatus) { CallStatus.Incoming -> stringResource( id = io.getstream.video.android.ui.common.R.string.stream_video_call_status_incoming, @@ -99,10 +96,7 @@ public fun ParticipantInformation( is CallStatus.Calling -> callStatus.duration }, - style = VideoTheme.typography.body, - fontSize = VideoTheme.dimens.onCallStatusTextSize, - fontWeight = FontWeight.Bold, - color = VideoTheme.colors.callDescription, + style = VideoTheme.typography.bodyM, textAlign = TextAlign.Center, ) } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt index 48f16992b2..866e90f6b6 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import io.getstream.log.StreamLog -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.video.VideoScalingType.Companion.toCommonScalingType import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState @@ -178,27 +178,18 @@ private fun DefaultMediaTrackFallbackContent( Column( modifier = modifier .fillMaxSize() - .background(VideoTheme.colors.appBackground) + .background(VideoTheme.colors.baseSheetTertiary) .testTag("video_renderer_fallback"), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - Image( - modifier = modifier.fillMaxSize(), - painter = painterResource( - id = io.getstream.video.android.ui.common.R.drawable.stream_video_ic_preview_avatar, - ), - contentScale = ContentScale.Crop, - contentDescription = null, - ) - Text( modifier = Modifier.padding(30.dp), text = stringResource( id = io.getstream.video.android.ui.common.R.string.stream_video_call_rendering_failed, call.sessionId, ), - color = VideoTheme.colors.textHighEmphasis, + color = VideoTheme.colors.basePrimary, textAlign = TextAlign.Center, fontSize = 14.sp, ) @@ -220,3 +211,12 @@ private fun VideoRendererPreview() { ) } } + +@Preview +@Composable +private fun VideoRendererFallbackPreview() { + StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) + VideoTheme { + DefaultMediaTrackFallbackContent(modifier = Modifier, call = previewCall) + } +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/utils/ImageUtils.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/utils/ImageUtils.kt index 712cd29c49..a6d5f0dcb1 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/utils/ImageUtils.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/utils/ImageUtils.kt @@ -18,35 +18,18 @@ package io.getstream.video.android.compose.utils import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import io.getstream.video.android.ui.common.R -import io.getstream.video.android.ui.common.util.adjustColorBrightness import kotlin.math.abs -private const val GradientDarkerColorFactor = 1.3f -private const val GradientLighterColorFactor = 0.7f - -/** - * Generates a gradient for an initials avatar based on the user initials. - * - * @param initials The user initials to use for gradient colors. - * @return The [Brush] that represents the gradient. - */ @Composable @ReadOnlyComposable -internal fun initialsGradient(initials: String): Brush { +internal fun initialsColors(initials: String): Pair { val gradientBaseColors = LocalContext.current.resources.getIntArray(R.array.stream_video_avatar_gradient_colors) val baseColorIndex = abs(initials.hashCode()) % gradientBaseColors.size - val baseColor = gradientBaseColors[baseColorIndex] - - return Brush.linearGradient( - listOf( - Color(adjustColorBrightness(baseColor, GradientDarkerColorFactor)), - Color(adjustColorBrightness(baseColor, GradientLighterColorFactor)), - ), - ) + val baseColor = Color(gradientBaseColors[baseColorIndex]) + return Pair(baseColor, baseColor.copy(alpha = 0.16f)) } diff --git a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/BaseComponentsTest.kt b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/BaseComponentsTest.kt new file mode 100644 index 0000000000..a5954c7fd5 --- /dev/null +++ b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/BaseComponentsTest.kt @@ -0,0 +1,470 @@ +/* + * 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.compose + +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.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccessAlarm +import androidx.compose.material.icons.filled.AddAlert +import androidx.compose.material.icons.filled.Camera +import androidx.compose.material.icons.filled.ExitToApp +import androidx.compose.material.icons.filled.GridView +import androidx.compose.material.icons.filled.GroupAdd +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.PhoneMissed +import androidx.compose.material.icons.filled.QuestionAnswer +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material.icons.filled.VideocamOff +import androidx.compose.material.icons.outlined.Phone +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import io.getstream.video.android.compose.base.BaseComposeTest +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.GenericContainer +import io.getstream.video.android.compose.ui.components.base.StreamBadgeBox +import io.getstream.video.android.compose.ui.components.base.StreamButton +import io.getstream.video.android.compose.ui.components.base.StreamIconButton +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.StreamToggleButton +import io.getstream.video.android.compose.ui.components.base.styling.ButtonStyles +import io.getstream.video.android.compose.ui.components.base.styling.StreamBadgeStyles +import io.getstream.video.android.compose.ui.components.base.styling.StreamTextFieldStyles +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize +import org.junit.Rule +import org.junit.Test + +internal class BaseComponentsTest : BaseComposeTest() { + + @get:Rule + val paparazzi = Paparazzi(deviceConfig = DeviceConfig.PIXEL_4A) + + override fun basePaparazzi(): Paparazzi = paparazzi + + @Test + fun `Regular icon buttons`() { + snapshot { + Column { + Row { + StreamIconButton( + icon = Icons.Default.GroupAdd, + style = ButtonStyles.primaryIconButtonStyle(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + icon = Icons.Default.ExitToApp, + style = ButtonStyles.secondaryIconButtonStyle(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + icon = Icons.Default.Settings, + style = ButtonStyles.tetriaryIconButtonStyle(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + icon = Icons.Default.PhoneMissed, + style = ButtonStyles.alertIconButtonStyle(), + ) + } + + Spacer(modifier = Modifier.size(48.dp)) + Row { + StreamIconButton( + enabled = false, + icon = Icons.Default.GroupAdd, + style = ButtonStyles.primaryIconButtonStyle(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + enabled = false, + icon = Icons.Default.ExitToApp, + style = ButtonStyles.secondaryIconButtonStyle(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + enabled = false, + icon = Icons.Default.Settings, + style = ButtonStyles.tetriaryIconButtonStyle(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + enabled = false, + icon = Icons.Default.PhoneMissed, + style = ButtonStyles.alertIconButtonStyle(), + ) + } + + Spacer(modifier = Modifier.size(48.dp)) + Row { + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + icon = Icons.Default.PhoneMissed, + style = ButtonStyles.alertIconButtonStyle(size = StyleSize.L), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + icon = Icons.Default.PhoneMissed, + style = ButtonStyles.alertIconButtonStyle(size = StyleSize.M), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamIconButton( + icon = Icons.Default.PhoneMissed, + style = ButtonStyles.alertIconButtonStyle(size = StyleSize.S), + ) + } + } + } + } + + @Test + fun `Regular buttons`() { + snapshot { + Column { + // Default + StreamButton( + text = "Primary Button", + style = ButtonStyles.primaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + text = "Secondary Button", + style = ButtonStyles.secondaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + text = "Tetriary Button", + style = ButtonStyles.tetriaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(48.dp)) + StreamButton( + text = "Alert Button", + style = ButtonStyles.alertButtonStyle(), + ) + Spacer(modifier = Modifier.height(48.dp)) + + // Disabled + StreamButton( + enabled = false, + text = "Primary Button", + style = ButtonStyles.primaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + enabled = false, + text = "Secondary Button", + style = ButtonStyles.secondaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + enabled = false, + text = "Tetriary Button", + style = ButtonStyles.tetriaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + enabled = false, + text = "Alert Button", + style = ButtonStyles.alertButtonStyle(), + ) + Spacer(modifier = Modifier.height(48.dp)) + } + } + } + + @Test + fun `Button with icons`() { + snapshot { + Column { + // With icon + StreamButton( + icon = Icons.Filled.AccessAlarm, + text = "Primary Button", + style = ButtonStyles.primaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + icon = Icons.Filled.AccessAlarm, + text = "Secondary Button", + style = ButtonStyles.secondaryButtonStyle(), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + icon = Icons.Filled.AccessAlarm, + text = "Tetriary Button", + style = ButtonStyles.tetriaryButtonStyle(), + ) + } + } + } + + @Test + fun `Different size buttons`() { + snapshot { + Column { + // Size + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + text = "Small Button", + style = ButtonStyles.secondaryButtonStyle(size = StyleSize.S), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + text = "Medium Button", + style = ButtonStyles.secondaryButtonStyle(size = StyleSize.M), + ) + Spacer(modifier = Modifier.height(24.dp)) + StreamButton( + text = "Large Button", + style = ButtonStyles.secondaryButtonStyle(size = StyleSize.L), + ) + } + } + } + + @Test + fun `Toggle icon buttons`() { + snapshot { + Row { + // Size + StreamIconToggleButton( + onStyle = ButtonStyles.primaryIconButtonStyle(), + offStyle = ButtonStyles.alertIconButtonStyle(), + onIcon = Icons.Default.Videocam, + offIcon = Icons.Default.VideocamOff, + ) + Spacer(modifier = Modifier.width(24.dp)) + StreamIconToggleButton( + toggleState = rememberUpdatedState(newValue = ToggleableState.On), + onStyle = ButtonStyles.primaryIconButtonStyle(), + offStyle = ButtonStyles.alertIconButtonStyle(), + onIcon = Icons.Default.Videocam, + offIcon = Icons.Default.VideocamOff, + ) + + Spacer(modifier = Modifier.width(24.dp)) + StreamBadgeBox( + style = StreamBadgeStyles.defaultBadgeStyle().copy( + color = VideoTheme.colors.alertCaution, + textStyle = VideoTheme.typography.labelXS.copy(color = Color.Black), + ), + text = "!", + ) { + StreamIconToggleButton( + enabled = false, + toggleState = rememberUpdatedState(newValue = ToggleableState.Off), + onStyle = ButtonStyles.secondaryIconButtonStyle(), + offStyle = ButtonStyles.alertIconButtonStyle(), + onIcon = Icons.Default.Videocam, + offIcon = Icons.Default.VideocamOff, + ) + } + } + } + } + + @Test + fun `Toggle buttons`() { + snapshot { + Column { + // Size + StreamToggleButton( + modifier = Modifier.fillMaxWidth(), + toggleState = rememberUpdatedState(newValue = ToggleableState.On), + onText = "Grid", + offText = "Grid", + onStyle = ButtonStyles.toggleButtonStyleOn(), + offStyle = ButtonStyles.toggleButtonStyleOff(), + onIcon = Icons.Filled.GridView, + offIcon = Icons.Filled.GridView, + ) + Spacer(modifier = Modifier.width(24.dp)) + StreamToggleButton( + modifier = Modifier.fillMaxWidth(), + toggleState = rememberUpdatedState(newValue = ToggleableState.Off), + onText = "Grid (On)", + offText = "Grid (Off)", + onStyle = ButtonStyles.toggleButtonStyleOn(), + offStyle = ButtonStyles.toggleButtonStyleOff(), + onIcon = Icons.Filled.GridView, + offIcon = Icons.Filled.GridView, + ) + } + } + } + + @Test + fun `Show progress into icon buttons`() { + snapshot { + Column { + StreamButton(text = "Progress", showProgress = true) + StreamIconButton( + icon = Icons.Default.Camera, + style = VideoTheme.styles.buttonStyles.secondaryIconButtonStyle(), + showProgress = true, + ) + } + } + } + + @Test + fun `Input fields`() { + snapshot { + Column { + // Empty + StreamTextField( + value = TextFieldValue(""), + placeholder = "Call ID", + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + StreamTextField( + value = TextFieldValue(""), + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + + StreamTextField( + icon = Icons.Outlined.Phone, + value = TextFieldValue(""), + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + + // Not empty + StreamTextField( + value = TextFieldValue("Some value"), + placeholder = "Call ID", + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + + StreamTextField( + icon = Icons.Outlined.Phone, + value = TextFieldValue("+ 123 456 789"), + placeholder = "Call ID", + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + + // Disabled + StreamTextField( + enabled = false, + value = TextFieldValue(""), + placeholder = "Call ID", + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + + // Error + StreamTextField( + error = true, + value = TextFieldValue("Wrong data"), + placeholder = "Call ID", + onValueChange = { }, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + + StreamTextField( + value = TextFieldValue(""), + placeholder = "Message", + onValueChange = { }, + minLines = 8, + style = StreamTextFieldStyles.defaultTextField(), + ) + Spacer(modifier = Modifier.size(16.dp)) + } + } + } + + @Test + fun `Generic container (for info messages)`() { + snapshot { + GenericContainer { + Text(text = "Contained text!", color = Color.White) + } + } + } + + @Test + fun `Badges with buttons`() { + snapshot { + Column { + StreamBadgeBox( + text = "!", + style = StreamBadgeStyles.defaultBadgeStyle(), + ) { + StreamIconButton( + icon = Icons.Default.AddAlert, + style = ButtonStyles.secondaryIconButtonStyle(), + ) + } + Spacer(modifier = Modifier.size(16.dp)) + StreamBadgeBox( + text = "10", + style = StreamBadgeStyles.defaultBadgeStyle(), + ) { + StreamButton( + icon = Icons.Default.Info, + text = "Secondary Button", + style = ButtonStyles.secondaryButtonStyle(), + ) + } + Spacer(modifier = Modifier.size(16.dp)) + StreamBadgeBox( + text = "10+", + style = StreamBadgeStyles.defaultBadgeStyle(), + ) { + StreamIconButton( + icon = Icons.Default.QuestionAnswer, + style = ButtonStyles.primaryIconButtonStyle(), + ) + } + Spacer(modifier = Modifier.size(16.dp)) + StreamBadgeBox( + showWithoutValue = false, + style = StreamBadgeStyles.defaultBadgeStyle(), + ) { + StreamIconButton( + icon = Icons.Default.QuestionAnswer, + style = ButtonStyles.primaryIconButtonStyle(), + ) + } + } + } + } +} diff --git a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/CallBackgroundTest.kt b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/CallBackgroundTest.kt deleted file mode 100644 index 60e981ce67..0000000000 --- a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/CallBackgroundTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.compose - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import app.cash.paparazzi.DeviceConfig -import app.cash.paparazzi.Paparazzi -import io.getstream.video.android.compose.base.BaseComposeTest -import io.getstream.video.android.compose.ui.components.avatar.Avatar -import io.getstream.video.android.compose.ui.components.background.CallBackground -import io.getstream.video.android.mock.previewMemberListState -import io.getstream.video.android.ui.common.R -import org.junit.Rule -import org.junit.Test - -internal class CallBackgroundTest : BaseComposeTest() { - - @get:Rule - val paparazzi = Paparazzi(deviceConfig = DeviceConfig.PIXEL_4A) - - override fun basePaparazzi(): Paparazzi = paparazzi - - @Test - fun `snapshot CallBackground composable`() { - snapshot { - CallBackground( - participants = previewMemberListState.take(1), - isIncoming = true, - ) { - Box(modifier = Modifier.align(Alignment.Center)) { - Avatar( - modifier = Modifier.size(72.dp), - initials = null, - previewPlaceholder = R.drawable.stream_video_call_sample, - ) - } - } - } - } -} diff --git a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/CallComponentsLandscapeTest.kt b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/CallComponentsLandscapeTest.kt deleted file mode 100644 index 282072021e..0000000000 --- a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/CallComponentsLandscapeTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.compose - -import app.cash.paparazzi.DeviceConfig -import app.cash.paparazzi.Paparazzi -import io.getstream.video.android.compose.base.BaseComposeTest -import io.getstream.video.android.compose.ui.components.call.controls.actions.LandscapeControlActions -import io.getstream.video.android.mock.previewCall -import org.junit.Rule -import org.junit.Test - -internal class CallComponentsLandscapeTest : BaseComposeTest() { - - @get:Rule - val paparazziLandscape = Paparazzi(deviceConfig = DeviceConfig.NEXUS_5_LAND) - - override fun basePaparazzi(): Paparazzi = paparazziLandscape - - @Test - fun `snapshot LandscapeCallControls composable`() { - snapshot { - LandscapeControlActions( - call = previewCall, - onCallAction = {}, - ) - } - } -} diff --git a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/CallComponentsPortraitTest.kt b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/CallComponentsPortraitTest.kt index 0c90ea0d9a..ba30403fed 100644 --- a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/CallComponentsPortraitTest.kt +++ b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/CallComponentsPortraitTest.kt @@ -21,8 +21,6 @@ import app.cash.paparazzi.Paparazzi import io.getstream.video.android.compose.base.BaseComposeTest import io.getstream.video.android.compose.ui.components.call.CallAppBar import io.getstream.video.android.compose.ui.components.call.controls.ControlActions -import io.getstream.video.android.compose.ui.components.call.controls.actions.RegularControlActions -import io.getstream.video.android.compose.ui.components.participants.ParticipantIndicatorIcon import io.getstream.video.android.mock.previewCall import org.junit.Rule import org.junit.Test @@ -44,7 +42,7 @@ internal class CallComponentsPortraitTest : BaseComposeTest() { @Test fun `snapshot RegularCallControls composable`() { snapshotWithDarkMode { - RegularControlActions( + ControlActions( call = previewCall, onCallAction = {}, ) @@ -60,11 +58,4 @@ internal class CallComponentsPortraitTest : BaseComposeTest() { ) } } - - @Test - fun `snapshot ParticipantIndicatorIcon composable`() { - snapshot { - ParticipantIndicatorIcon(number = 42) {} - } - } } diff --git a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/IndicatorsTest.kt b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/IndicatorsTest.kt index 4690eb4535..2fd9ecf4b6 100644 --- a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/IndicatorsTest.kt +++ b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/IndicatorsTest.kt @@ -23,7 +23,7 @@ import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi import io.getstream.video.android.compose.base.BaseComposeTest import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantLabel -import io.getstream.video.android.compose.ui.components.connection.NetworkQualityIndicator +import io.getstream.video.android.compose.ui.components.indicator.NetworkQualityIndicator import io.getstream.video.android.compose.ui.components.indicator.SoundIndicator import io.getstream.video.android.core.model.NetworkQuality import io.getstream.video.android.mock.previewCall diff --git a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/ParticipantLandscapeTest.kt b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/ParticipantLandscapeTest.kt index 99439e432c..f7002e6c31 100644 --- a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/ParticipantLandscapeTest.kt +++ b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/ParticipantLandscapeTest.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.unit.IntSize import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi import io.getstream.video.android.compose.base.BaseComposeTest -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.internal.LandscapeScreenSharingVideoRenderer import io.getstream.video.android.compose.ui.components.call.renderer.internal.LandscapeVideoRenderer import io.getstream.video.android.compose.ui.components.call.renderer.internal.LazyRowVideoRenderer @@ -52,7 +52,7 @@ internal class ParticipantLandscapeTest : BaseComposeTest() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { LandscapeVideoRenderer( call = previewCall, @@ -74,7 +74,7 @@ internal class ParticipantLandscapeTest : BaseComposeTest() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { LandscapeVideoRenderer( call = previewCall, @@ -96,7 +96,7 @@ internal class ParticipantLandscapeTest : BaseComposeTest() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { LandscapeVideoRenderer( call = previewCall, @@ -118,7 +118,7 @@ internal class ParticipantLandscapeTest : BaseComposeTest() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { LandscapeVideoRenderer( call = previewCall, @@ -140,7 +140,7 @@ internal class ParticipantLandscapeTest : BaseComposeTest() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { LandscapeVideoRenderer( call = previewCall, @@ -162,7 +162,7 @@ internal class ParticipantLandscapeTest : BaseComposeTest() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { LandscapeVideoRenderer( call = previewCall, diff --git a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/ParticipantsPortraitTest.kt b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/ParticipantsPortraitTest.kt index 3955cb9def..e6a708f2ba 100644 --- a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/ParticipantsPortraitTest.kt +++ b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/ParticipantsPortraitTest.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.unit.IntSize import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi import io.getstream.video.android.compose.base.BaseComposeTest -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.FloatingParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantVideo import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantVideoRenderer @@ -175,7 +175,7 @@ internal class ParticipantsPortraitTest : BaseComposeTest() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { PortraitVideoRenderer( call = previewCall, @@ -197,7 +197,7 @@ internal class ParticipantsPortraitTest : BaseComposeTest() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { PortraitVideoRenderer( call = previewCall, @@ -219,7 +219,7 @@ internal class ParticipantsPortraitTest : BaseComposeTest() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { PortraitVideoRenderer( call = previewCall, @@ -241,7 +241,7 @@ internal class ParticipantsPortraitTest : BaseComposeTest() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { PortraitVideoRenderer( call = previewCall, @@ -263,7 +263,7 @@ internal class ParticipantsPortraitTest : BaseComposeTest() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { PortraitVideoRenderer( call = previewCall, @@ -285,7 +285,7 @@ internal class ParticipantsPortraitTest : BaseComposeTest() { val participants = previewParticipantsList Box( - modifier = Modifier.background(color = VideoTheme.colors.appBackground), + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), ) { PortraitVideoRenderer( call = previewCall, diff --git a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/base/BaseComposeTest.kt b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/base/BaseComposeTest.kt index b76defa8f6..9da8502ce1 100644 --- a/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/base/BaseComposeTest.kt +++ b/stream-video-android-ui-compose/src/test/kotlin/io/getstream/video/android/compose/base/BaseComposeTest.kt @@ -23,7 +23,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import app.cash.paparazzi.Paparazzi -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.LocalAvatarPreviewPlaceholder import io.getstream.video.android.mock.StreamPreviewDataUtils diff --git a/stream-video-android-ui-core/src/main/res/drawable/stream_video_ic_join_call.xml b/stream-video-android-ui-core/src/main/res/drawable/stream_video_ic_join_call.xml new file mode 100644 index 0000000000..fedea6e86d --- /dev/null +++ b/stream-video-android-ui-core/src/main/res/drawable/stream_video_ic_join_call.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/stream-video-android-ui-core/src/main/res/values/colors-legacy.xml b/stream-video-android-ui-core/src/main/res/values/colors-legacy.xml new file mode 100644 index 0000000000..5cc49f8627 --- /dev/null +++ b/stream-video-android-ui-core/src/main/res/values/colors-legacy.xml @@ -0,0 +1,105 @@ + + + + + #000000 + #72767E + #B4B7BB + #DBDDE1 + #E9EAED + #FFFFFF + #FFFFFF + #E9F1FF + #80000000 + #99000000 + #005FFF + #A60C0D0E + #A60C0D0E + #FFFFFF + #D9000000 + #FCFCFC + #FF3742 + #20E070 + #005FFF + #123D82 + #1E262E + #FBF4DD + #FFFFFF + #545A64 + #272A30 + #FFFFFF + #000000 + #FFFFFF + #FFFFFF + #000000 + #4C525C + #FFFFFF + #1E262E + #005FFF + #A60C0D0E + #00E2A1 + #FFFFFF + #DC433B + #FF3742 + + + #FFFFFF + #72767E + #4C525C + #272A30 + #1C1E22 + #000000 + #121416 + #00193D + #33000000 + #99FFFFFF + #337EFF + #FF3742 + #20E070 + #302D22 + #FFFFFF + #FFFFFF + #000000 + #000000 + #FFFFFF + #4C525C + #FFFFFF + #FFFFFF + #005FFF + #A60C0D0E + #FF3742 + #A60C0D0E + #A60C0D0E + + + #FF8A65 + #81C784 + #4FC3F7 + #A1887F + #FFF176 + #E57373 + + @color/stream_video_brand_cyan + @color/stream_video_brand_green + @color/stream_video_brand_primary + @color/stream_video_brand_primary_lt + @color/stream_video_brand_red + @color/stream_video_brand_red_lt + @color/stream_video_brand_violet + @color/stream_video_brand_yellow + + diff --git a/stream-video-android-ui-core/src/main/res/values/colors.xml b/stream-video-android-ui-core/src/main/res/values/colors.xml index 8a57f152b7..69b7a1360c 100644 --- a/stream-video-android-ui-core/src/main/res/values/colors.xml +++ b/stream-video-android-ui-core/src/main/res/values/colors.xml @@ -15,89 +15,55 @@ limitations under the License. --> - - #000000 - #72767E - #B4B7BB - #DBDDE1 - #E9EAED - #FFFFFF - #FFFFFF - #E9F1FF - #80000000 - #99000000 - #005FFF - #A60C0D0E - #A60C0D0E - #FFFFFF - #D9000000 - #FCFCFC - #FF3742 - #20E070 - #005FFF - #123D82 - #1E262E - #FBF4DD - #FFFFFF - #545A64 - #272A30 - #FFFFFF - #000000 - #FFFFFF - #FFFFFF - #000000 - #4C525C - #FFFFFF - #1E262E - #005FFF - #A60C0D0E - #00E2A1 - #FFFFFF - #DC433B - #FF3742 + + #005FFF + #4C8FFF + #123D82 + #1B2C43 + #1E262E29 + #69E5F6 + #00E2A1 + #FFD646 + #DC433B + #E36962 + #6A3233 + #31292F + #B38AF8 - - #FFFFFF - #72767E - #4C525C - #272A30 - #1C1E22 - #000000 - #121416 - #00193D - #33000000 - #99FFFFFF - #337EFF - #FF3742 - #20E070 - #302D22 - #FFFFFF - #FFFFFF - #000000 - #000000 - #FFFFFF - #4C525C - #FFFFFF - #FFFFFF - #005FFF - #A60C0D0E - #FF3742 - #A60C0D0E - #A60C0D0E + + #E3E4E5 + #979CA0 + #4C535B + #656B72 + #7E8389 + #323B44 + #101213 + #0C0D0E + #19232D + #A60C0D0E + #0C0D0EA6 - - #FF8A65 - #81C784 - #4FC3F7 - #A1887F - #FFF176 - #E57373 - - @color/stream_video_avatar_gradient_orange - @color/stream_video_avatar_gradient_green - @color/stream_video_avatar_gradient_blue - @color/stream_video_avatar_gradient_brown - @color/stream_video_avatar_gradient_yellow - @color/stream_video_avatar_gradient_red - - + + @color/stream_video_base_sheet_tetriary + @color/stream_video_base_sheet_secondary + @color/stream_video_brand_secondary_transparent + @color/stream_video_brand_primary + @color/stream_video_brand_primary_dk + @color/stream_video_brand_secondary + @color/stream_video_brand_red + @color/stream_video_brand_red_dk + @color/stream_video_brand_maroon + + + @color/stream_video_base_primary + @color/stream_video_base_quaternary + @color/stream_video_brand_primary + @color/stream_video_brand_red + @color/stream_video_base_quinary + + + @color/stream_video_brand_green + @color/stream_video_brand_yellow + @color/stream_video_brand_red + + \ No newline at end of file diff --git a/stream-video-android-ui-core/src/main/res/values/dimens-legacy.xml b/stream-video-android-ui-core/src/main/res/values/dimens-legacy.xml new file mode 100644 index 0000000000..244d982672 --- /dev/null +++ b/stream-video-android-ui-core/src/main/res/values/dimens-legacy.xml @@ -0,0 +1,122 @@ + + + + + 86dp + 172dp + 4dp + 80dp + 64dp + 32dp + 64dp + 48dp + 20dp + 20dp + 65dp + 64dp + 44dp + 12dp + 0dp + 0dp + 8dp + 0dp + 8dp + 8dp + 18dp + 6dp + 96dp + 50dp + 45dp + 64dp + 8dp + 64dp + 56dp + 40dp + 56dp + 32dp + 16dp + 16dp + 200dp + 140dp + 16dp + 16dp + 10dp + 2dp + 2dp + 22dp + 2dp + 1dp + 24dp + 18dp + 2dp + 2dp + 1dp + 2dp + 2dp + 32dp + 8dp + 4dp + 8dp + 120dp + 4dp + 110dp + 125dp + 8dp + 4dp + 16dp + 16dp + 16dp + 16dp + 16dp + 35dp + 5dp + 40dp + 280dp + 12dp + 32dp + 24dp + 44dp + 84dp + 8dp + 4dp + 5dp + 20dp + 5dp + 22dp + 40dp + + 24sp + 34sp + 18sp + 23sp + 14sp + 12sp + 20sp + 10sp + 16sp + 10sp + 17sp + + 34sp + 24sp + 20sp + + 0.6 + 1.0 + 0.6 + + \ No newline at end of file diff --git a/stream-video-android-ui-core/src/main/res/values/dimens.xml b/stream-video-android-ui-core/src/main/res/values/dimens.xml index 244d982672..e6bf9e0328 100644 --- a/stream-video-android-ui-core/src/main/res/values/dimens.xml +++ b/stream-video-android-ui-core/src/main/res/values/dimens.xml @@ -15,108 +15,52 @@ limitations under the License. --> + + 100dp + 84dp + 44dp + 32dp + 24dp + 16dp + 8dp + 4dp + 2dp - 86dp - 172dp - 4dp - 80dp - 64dp - 32dp - 64dp - 48dp - 20dp - 20dp - 65dp - 64dp - 44dp - 12dp - 0dp - 0dp - 8dp - 0dp - 8dp - 8dp - 18dp - 6dp - 96dp - 50dp - 45dp - 64dp - 8dp - 64dp - 56dp - 40dp - 56dp - 32dp - 16dp - 16dp - 200dp - 140dp - 16dp - 16dp - 10dp - 2dp - 2dp - 22dp - 2dp - 1dp - 24dp - 18dp - 2dp - 2dp - 1dp - 2dp - 2dp - 32dp - 8dp - 4dp - 8dp - 120dp - 4dp - 110dp - 125dp - 8dp - 4dp - 16dp - 16dp - 16dp - 16dp - 16dp - 35dp - 5dp - 40dp - 280dp - 12dp - 32dp - 24dp - 44dp - 84dp - 8dp - 4dp - 5dp - 20dp - 5dp - 22dp - 40dp + + @dimen/stream_video_generic_xl + @dimen/stream_video_generic_l + @dimen/stream_video_generic_m + @dimen/stream_video_generic_s - 24sp - 34sp - 18sp - 23sp - 14sp - 12sp - 20sp - 10sp - 16sp - 10sp - 17sp + + @dimen/stream_video_generic_xl + @dimen/stream_video_generic_l + @dimen/stream_video_generic_m + @dimen/stream_video_generic_s + @dimen/stream_video_generic_xs + @dimen/stream_video_generic_xxs - 34sp - 24sp - 20sp - - 0.6 - 1.0 - 0.6 + + @dimen/stream_video_generic_xxl + @dimen/stream_video_generic_xl + @dimen/stream_video_generic_l + @dimen/stream_video_spacing_s + @dimen/stream_video_spacing_m + @dimen/stream_video_spacing_s + @dimen/stream_video_spacing_m + @dimen/stream_video_spacing_s + + 93sp + 48sp + 24sp + 20sp + 16sp + 13sp + 43sp + 28sp + 24sp + 20sp + 16sp + 13sp \ No newline at end of file From dc3d774d46c9632365c9f8b4c8f3f75ebf52d0a2 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Wed, 7 Feb 2024 14:11:15 +0100 Subject: [PATCH 05/13] Improve aspect ratio of floating participant (#1010) --- .../ui/components/call/renderer/FloatingParticipantVideo.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/FloatingParticipantVideo.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/FloatingParticipantVideo.kt index 77598e998b..369c270e73 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/FloatingParticipantVideo.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/FloatingParticipantVideo.kt @@ -147,8 +147,8 @@ public fun BoxScope.FloatingParticipantVideo( modifier = Modifier .align(alignment) .size( - height = VideoTheme.dimens.genericMax * 1.8f, - width = VideoTheme.dimens.genericMax, + height = VideoTheme.dimens.genericMax * 2.2f, + width = VideoTheme.dimens.genericMax * 1.5f, ) .offset { IntOffset(offset.x.toInt(), offset.y.toInt()) } .pointerInput(parentBounds) { From 1cadf99581511a86fe0bd051004afd44a7059b6d Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Wed, 7 Feb 2024 17:11:01 +0100 Subject: [PATCH 06/13] Allow for automatic env switching and link handling depending on host (#1009) --- demo-app/src/main/AndroidManifest.xml | 2 + .../video/android/DeeplinkingActivity.kt | 16 +- .../video/android/ui/call/ShareCall.kt | 7 +- .../video/android/ui/login/LoginScreen.kt | 19 +- .../video/android/ui/login/LoginViewModel.kt | 2 +- .../android/util/StreamVideoInitHelper.kt | 8 + .../video/android/util/config/AppConfig.kt | 168 ++++++------------ .../video/android/util/config/types/Flavor.kt | 26 --- .../util/config/types/StreamEnvironment.kt | 3 +- .../util/config/types/StreamRemoteConfig.kt | 26 --- .../util/config/types/SupportedLogins.kt | 26 --- .../main/res/xml/remote_config_defaults.xml | 22 --- 12 files changed, 91 insertions(+), 234 deletions(-) delete mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/Flavor.kt delete mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamRemoteConfig.kt delete mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/SupportedLogins.kt delete mode 100644 demo-app/src/main/res/xml/remote_config_defaults.xml diff --git a/demo-app/src/main/AndroidManifest.xml b/demo-app/src/main/AndroidManifest.xml index e92c2aa0d4..bb41e838a9 100644 --- a/demo-app/src/main/AndroidManifest.xml +++ b/demo-app/src/main/AndroidManifest.xml @@ -89,6 +89,8 @@ + + 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 818369b232..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 @@ -43,6 +43,8 @@ import io.getstream.video.android.model.StreamCallId import io.getstream.video.android.ui.call.CallActivity 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 @@ -105,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( @@ -114,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) } }, ) @@ -157,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/ui/call/ShareCall.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ShareCall.kt index a19ea160a1..4bada6e585 100644 --- 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 @@ -49,7 +49,6 @@ 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.tooling.util.StreamEnvironments import io.getstream.video.android.util.config.types.StreamEnvironment @Composable @@ -61,11 +60,7 @@ public fun ShareCallWithOthers( context: Context, ) { ShareSettingsBox(modifier, call, clipboardManager) { - val link = if (env.value?.env == StreamEnvironments.demo) { - "https://getstream.io/video/demos/join/${call.id}" - } else { - "https://${env.value?.env}.getstream.io/video/demos/join/${call.id}" - } + val link = "${env.value?.sharelink}${call.id}" val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TEXT, link) 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 73c6cdff5c..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 @@ -33,6 +33,7 @@ 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.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions @@ -42,7 +43,6 @@ 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.material.icons.outlined.Settings import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -92,7 +92,6 @@ 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.Flavor import io.getstream.video.android.util.config.types.StreamEnvironment /** @@ -116,8 +115,8 @@ fun LoginScreen( ) } val selectedEnv by AppConfig.currentEnvironment.collectAsStateWithLifecycle() - val availableEnvs by AppConfig.availableEnvironments.collectAsStateWithLifecycle() - val availableLogins by AppConfig.availableLogins.collectAsStateWithLifecycle() + val availableEnvs by remember { mutableStateOf(AppConfig.availableEnvironments) } + val availableLogins = listOf("google", "email", "guest") var isShowingEmailLoginDialog by remember { mutableStateOf(false) } @@ -385,7 +384,7 @@ fun SelectableDialog( Modifier.background( color = VideoTheme.colors.baseSheetTertiary, shape = VideoTheme.shapes.dialog, - ), + ).width(180.dp), ) { items.forEach { item -> StreamButton( @@ -396,7 +395,7 @@ fun SelectableDialog( showDialog = !showDialog }, style = ButtonStyles.tetriaryButtonStyle(), - modifier = Modifier.padding(8.dp), + modifier = Modifier.padding(horizontal = 8.dp).fillMaxWidth(), ) } } @@ -467,7 +466,7 @@ private fun HandleLoginUiStates( @Composable private fun LoginScreenPreview() { VideoTheme { - val env = StreamEnvironment("demo", "Demo", listOf(Flavor("development", true))) + val env = StreamEnvironment(env = "demo", displayName = "Demo") LoginContent( autoLogIn = false, isLoading = false, @@ -482,7 +481,7 @@ private fun LoginScreenPreview() { @Composable private fun EmailDialogPreview() { VideoTheme { - val env = StreamEnvironment("demo", "Demo", listOf(Flavor("development", true))) + val env = StreamEnvironment(env = "demo", displayName = "Demo") EmailLoginDialog() } } @@ -491,8 +490,8 @@ private fun EmailDialogPreview() { @Composable private fun SelectEnvOption() { VideoTheme { - val env = StreamEnvironment("demo", "Demo", listOf(Flavor("development", true))) - val env2 = StreamEnvironment("pronto", "Pronto", listOf(Flavor("development", true))) + val env = StreamEnvironment(env = "demo", displayName = "Demo") + val env2 = StreamEnvironment(env = "pronto", displayName = "Pronto") SelectableDialog( items = listOf(env, env2), 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 2252b479d8..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 @@ -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/util/StreamVideoInitHelper.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt index ec56459b9c..a8b359046d 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt @@ -57,6 +57,14 @@ object StreamVideoInitHelper { context = appContext.applicationContext } + suspend fun reloadSdk( + dataStore: StreamUserDataStore, + useRandomUserAsFallback: Boolean = true, + ) { + StreamVideo.removeClient() + loadSdk(dataStore, useRandomUserAsFallback) + } + /** * A helper function that will initialise the [StreamVideo] SDK and also the [ChatClient]. * Set [useRandomUserAsFallback] to true if you want to use a guest fallback if the user is not diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/AppConfig.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/AppConfig.kt index be83b462ae..274c23db1a 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/AppConfig.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/AppConfig.kt @@ -19,27 +19,19 @@ package io.getstream.video.android.util.config import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.SharedPreferences +import android.net.Uri import androidx.core.content.edit -import com.google.firebase.ktx.Firebase -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import com.google.firebase.remoteconfig.ktx.remoteConfig -import com.google.firebase.remoteconfig.ktx.remoteConfigSettings import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import io.getstream.log.taggedLogger -import io.getstream.video.android.BuildConfig -import io.getstream.video.android.R -import io.getstream.video.android.util.config.types.Flavor import io.getstream.video.android.util.config.types.StreamEnvironment -import io.getstream.video.android.util.config.types.StreamRemoteConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -import java.util.concurrent.Executors /** * Main entry point for remote / local configuration @@ -48,25 +40,40 @@ import java.util.concurrent.Executors object AppConfig { // Constants private val logger by taggedLogger("RemoteConfig") - private const val APP_CONFIG_KEY = "appconfig" private const val SHARED_PREF_NAME = "stream_demo_app" - private const val SELECTED_ENV = "selected_env" + private const val SELECTED_ENV = "selected_env_v2" // Data - private lateinit var config: StreamRemoteConfig private lateinit var environment: StreamEnvironment private lateinit var prefs: SharedPreferences // State of config values - public val currentEnvironment = MutableStateFlow(null) - public val availableEnvironments = MutableStateFlow>(arrayListOf()) - public val availableLogins = MutableStateFlow>(arrayListOf()) - - // Utils + val currentEnvironment = MutableStateFlow(null) + val availableEnvironments = listOf( + StreamEnvironment( + env = "pronto", + aliases = listOf("stream-calls-dogfood"), + displayName = "Pronto", + sharelink = "https://pronto.getstream.io/join/", + ), + StreamEnvironment( + env = "demo", + aliases = listOf(""), + displayName = "Demo", + sharelink = "https://getstream.io/video/demos/join/", + ), + StreamEnvironment( + env = "staging", + aliases = emptyList(), + displayName = "Staging", + sharelink = "https://staging.getstream.io/join/", + ), + ) + + // Utilities private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() // API - /** * Setup the remote configuration. * Will automatically put config into [AppConfig.config] @@ -82,51 +89,21 @@ object AppConfig { ) { // Load prefs prefs = context.getSharedPreferences(SHARED_PREF_NAME, MODE_PRIVATE) - - // Initialize local and default values - val remoteConfig = initializeRemoteConfig() - - // Fetch remote - remoteConfig.fetchAndActivate() - .addOnCompleteListener(Executors.newSingleThreadExecutor()) { task -> - if (task.isSuccessful) { - logger.v { "Updated remote config values" } - } else { - logger.e { "Update of remote config failed." } - } - try { - // Parse config - val parsed = parseConfig(remoteConfig) - config = parsed!! - - // Update available logins - availableLogins.value = config.supportedLogins.firstOrNull { - it.flavor.contains(BuildConfig.FLAVOR) - }?.logins ?: arrayListOf("email") - - // Select environment - val jsonAdapter: JsonAdapter = moshi.adapter() - val selectedEnvData = prefs.getString(SELECTED_ENV, null) - var selectedEnvironment = selectedEnvData?.let { - jsonAdapter.fromJson(it) - } - if (selectedEnvironment?.isForFlavor(BuildConfig.FLAVOR) != true) { - // We may have selected environment previously which is no longer available - selectedEnvironment = null - } - val which = selectedEnvironment ?: config.environments.default(BuildConfig.FLAVOR) - selectEnv(which) - availableEnvironments.value = config.environments.filter { - it.isForFlavor(BuildConfig.FLAVOR) - } - currentEnvironment.value = which - coroutineScope.launch { - onLoaded() - } - } catch (e: Exception) { - logger.e(e) { "Failed to parse remote config. Deeplinks not working!" } - } + try { + val jsonAdapter: JsonAdapter = moshi.adapter() + val selectedEnvData = prefs.getString(SELECTED_ENV, null) + val selectedEnvironment = selectedEnvData?.let { + jsonAdapter.fromJson(it) } + val which = selectedEnvironment ?: availableEnvironments[0] + selectEnv(which) + currentEnvironment.value = which + coroutineScope.launch { + onLoaded() + } + } catch (e: Exception) { + logger.e(e) { "Failed to parse remote config. Deeplinks not working!" } + } } /** @@ -135,15 +112,9 @@ object AppConfig { * @param which environment to select */ fun selectEnv(which: StreamEnvironment) { - val currentFlavor = BuildConfig.FLAVOR val jsonAdapter: JsonAdapter = moshi.adapter() - - val selectedEnvironment = which.takeIf { - config.environments.containsForFlavor(it.env!!, currentFlavor) - } - // Select default environment from config if none is in prefs - environment = selectedEnvironment ?: config.environments.default(currentFlavor) + environment = which // Update selected env prefs.edit(commit = true) { putString(SELECTED_ENV, jsonAdapter.toJson(environment)) @@ -151,51 +122,24 @@ object AppConfig { currentEnvironment.value = environment } - // Internal logic - private fun initializeRemoteConfig(): FirebaseRemoteConfig { - val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig - val configSettings = remoteConfigSettings { - minimumFetchIntervalInSeconds = 3600 - } - remoteConfig.setConfigSettingsAsync(configSettings) - remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults) - return remoteConfig - } - - @OptIn(ExperimentalStdlibApi::class) - private fun parseConfig(remoteConfig: FirebaseRemoteConfig): StreamRemoteConfig? { - val value = remoteConfig.getString(APP_CONFIG_KEY) - val jsonAdapter: JsonAdapter = moshi.adapter() - return jsonAdapter.fromJson(value) - } - - private fun List.containsForFlavor(name: String, flavor: String): Boolean { - val found = this.find { - it.env == name && it.flavors.containsFlavorName(flavor) - } - return found != null - } - - private fun List.containsFlavorName(name: String): Boolean { - val found = this.find { - it.flavor!! == name + fun List.fromUri(env: Uri): StreamEnvironment? { + val environmentName = env.extractEnvironment() + return environmentName?.let { name -> + firstOrNull { streamEnv -> + streamEnv.env == name || streamEnv.aliases.contains(name) + } } - return found != null - } - - private fun StreamEnvironment.isForFlavor(flavor: String): Boolean { - return flavors.find { it.flavor == flavor } != null - } - - private fun StreamEnvironment.isDefaultForFlavor(flavor: String): Boolean { - return flavors.find { it.flavor == flavor }?.default == true } - private fun List.default(currentFlavor: String): StreamEnvironment { - return findLast { env -> - env.isDefaultForFlavor(currentFlavor) - } ?: config.environments.find { - it.isForFlavor(currentFlavor) - } ?: config.environments.first() + private fun Uri?.extractEnvironment(): String? { + // Extract the host from the Uri + val host = this?.host ?: return null + // Split the host by "." and return the first part + val parts = host.split(".") + // 0 | 1 | 2 + // | getstream | io + // pronto | getstream | io + // stream-call-dogfood | vercel | app + return if (parts.size > 2) parts[0] else "" } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/Flavor.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/Flavor.kt deleted file mode 100644 index 910f0532ab..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/Flavor.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. - * - * Licensed under the Stream License; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://github.com/GetStream/stream-video-android/blob/main/LICENSE - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.getstream.video.android.util.config.types - -import androidx.annotation.Keep -import com.squareup.moshi.Json - -@Keep -data class Flavor( - @Json(name = "flavor") var flavor: String? = null, - @Json(name = "default") var default: Boolean? = null, -) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamEnvironment.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamEnvironment.kt index d54441dbc7..1b4a3e540d 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamEnvironment.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamEnvironment.kt @@ -22,6 +22,7 @@ import com.squareup.moshi.Json @Keep data class StreamEnvironment( @Json(name = "env") var env: String, + @Json(name = "aliases") var aliases: List = emptyList(), @Json(name = "displayName") var displayName: String, - @Json(name = "flavors") var flavors: List = arrayListOf(), + @Json(name = "sharelink") var sharelink: String? = null, ) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamRemoteConfig.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamRemoteConfig.kt deleted file mode 100644 index e5f5f27d45..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/StreamRemoteConfig.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. - * - * Licensed under the Stream License; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://github.com/GetStream/stream-video-android/blob/main/LICENSE - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.getstream.video.android.util.config.types - -import androidx.annotation.Keep -import com.squareup.moshi.Json - -@Keep -data class StreamRemoteConfig( - @Json(name = "supportedLogins") var supportedLogins: List = arrayListOf(), - @Json(name = "environments") var environments: List = arrayListOf(), -) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/SupportedLogins.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/SupportedLogins.kt deleted file mode 100644 index 59c03e5f8d..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/config/types/SupportedLogins.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. - * - * Licensed under the Stream License; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://github.com/GetStream/stream-video-android/blob/main/LICENSE - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.getstream.video.android.util.config.types - -import androidx.annotation.Keep -import com.squareup.moshi.Json - -@Keep -data class SupportedLogins( - @Json(name = "flavor") var flavor: List = arrayListOf(), - @Json(name = "logins") var logins: List = arrayListOf(), -) diff --git a/demo-app/src/main/res/xml/remote_config_defaults.xml b/demo-app/src/main/res/xml/remote_config_defaults.xml deleted file mode 100644 index 2efdfbe6bb..0000000000 --- a/demo-app/src/main/res/xml/remote_config_defaults.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - appconfig - {"supportedLogins":[{"flavor":["development","production"],"logins":["google","email","guest"]}],"environments":[{"env":"demo","displayName":"Demo","flavors":[{"flavor":"development","default":false},{"flavor":"production","default":true}]},{"env":"pronto","displayName":"Pronto","flavors":[{"flavor":"development","default":true}]}]} - - \ No newline at end of file From 5c36298fc0a79bdc21be07f5cb07ffb5b54107d3 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 12 Feb 2024 10:54:08 +0100 Subject: [PATCH 07/13] Improve menu and submenu behavior (#1011) --- .../video/android/ui/call/CallScreen.kt | 1 + .../video/android/ui/call/SettingsMenu.kt | 337 ------------------ .../video/android/ui/menu/MenuDefinitions.kt | 161 +++++++++ .../video/android/ui/menu/SettingsMenu.kt | 234 ++++++++++++ .../video/android/ui/menu/base/DynamicMenu.kt | 188 ++++++++++ .../video/android/ui/menu/base/MenuTypes.kt | 35 ++ 6 files changed, 619 insertions(+), 337 deletions(-) delete mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/ui/call/SettingsMenu.kt create mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt create mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt create mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt create mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/MenuTypes.kt 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 1eb567bc4e..5e05efd7fe 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 @@ -97,6 +97,7 @@ 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 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 e2ce705ab8..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/SettingsMenu.kt +++ /dev/null @@ -1,337 +0,0 @@ -/* - * 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.app.Activity -import android.graphics.Bitmap -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.layout.Column -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.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.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.vectorResource -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.compose.ui.window.PopupProperties -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.getstream.video.android.compose.theme.base.VideoTheme -import io.getstream.video.android.compose.ui.components.base.StreamToggleButton -import io.getstream.video.android.compose.ui.components.base.styling.StyleSize -import io.getstream.video.android.core.Call -import io.getstream.video.android.core.call.audio.AudioFilter -import io.getstream.video.android.core.call.video.BitmapVideoFilter -import io.getstream.video.android.core.mapper.ReactionMapper -import io.getstream.video.android.mock.StreamPreviewDataUtils -import io.getstream.video.android.mock.previewCall -import io.getstream.video.android.tooling.extensions.toPx -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" - } - - // Define the actions as lambda variables - val onReactionsClick: () -> Unit = { - onDismissed() - onShowReactionsMenu() - } - - val onScreenShareClick: () -> Unit = { - if (!isScreenSharing) { - scope.launch { - val mediaProjectionManager = - context.getSystemService(MediaProjectionManager::class.java) - screenSharePermissionResult.launch( - mediaProjectionManager.createScreenCaptureIntent(), - ) - } - } else { - call.stopScreenSharing() - } - } - - val onSwitchMicrophoneClick: () -> Unit = { - onDismissed.invoke() - onDisplayAvailableDevice.invoke() - } - - val onToggleBackgroundBlurClick: () -> Unit = { - onToggleBackgroundBlur() - - if (call.videoFilter == null) { - call.videoFilter = object : BitmapVideoFilter() { - val filter = BlurredBackgroundVideoFilter() - - override fun filter(bitmap: Bitmap) { - filter.applyFilter(bitmap) - } - } - } else { - call.videoFilter = null - } - } - - val onToggleAudioFilterClick: () -> Unit = { - if (call.audioFilter == null) { - call.audioFilter = object : AudioFilter { - override fun filter( - audioFormat: Int, - channelCount: Int, - sampleRate: Int, - sampleData: ByteBuffer, - ) { - SampleAudioFilter.toRoboticVoice(sampleData, channelCount, 0.8f) - } - } - } else { - call.audioFilter = null - } - } - - val onRestartSubscriberIceClick: () -> Unit = { - call.debug.restartSubscriberIce() - onDismissed.invoke() - Toast.makeText(context, "Restart Subscriber Ice", Toast.LENGTH_SHORT).show() - } - - val onRestartPublisherIceClick: () -> Unit = { - call.debug.restartPublisherIce() - onDismissed.invoke() - Toast.makeText(context, "Restart Publisher Ice", Toast.LENGTH_SHORT).show() - } - - val onKillSfuWsClick: () -> Unit = { - call.debug.doFullReconnection() - onDismissed.invoke() - Toast.makeText(context, "Killing SFU WS. Should trigger reconnect...", Toast.LENGTH_SHORT) - .show() - } - - val onSwitchSfuClick: () -> Unit = { - call.debug.switchSfu() - onDismissed.invoke() - Toast.makeText(context, "Switch sfu", Toast.LENGTH_SHORT).show() - } - - SettingsMenuItems( - onScreenShareClick = onScreenShareClick, - onSwitchMicrophoneClick = onSwitchMicrophoneClick, - onToggleBackgroundBlurClick = onToggleBackgroundBlurClick, - onToggleAudioFilterClick = onToggleAudioFilterClick, - onRestartSubscriberIceClick = onRestartSubscriberIceClick, - onRestartPublisherIceClick = onRestartPublisherIceClick, - onKillSfuWsClick = onKillSfuWsClick, - onSwitchSfuClick = onSwitchSfuClick, - showDebugOptions = showDebugOptions, - isBackgroundBlurEnabled = isBackgroundBlurEnabled, - onShowStates = onShowCallStats, - onDismissed = onDismissed, - reactionsMenu = { - ReactionsMenu(call = call, reactionMapper = ReactionMapper.defaultReactionMapper()) { - onDismissed() - } - }, - ) -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun SettingsMenuItems( - isBackgroundBlurEnabled: Boolean, - onScreenShareClick: () -> Unit, - onSwitchMicrophoneClick: () -> Unit, - onToggleBackgroundBlurClick: () -> Unit, - onToggleAudioFilterClick: () -> Unit, - onRestartSubscriberIceClick: () -> Unit, - onRestartPublisherIceClick: () -> Unit, - onKillSfuWsClick: () -> Unit, - onSwitchSfuClick: () -> Unit, - showDebugOptions: Boolean, - onDismissed: () -> Unit, - reactionsMenu: @Composable () -> Unit, - onShowStates: () -> Unit, -) { - Popup( - - offset = IntOffset( - 0, - -(VideoTheme.dimens.componentHeightL + VideoTheme.dimens.spacingS).toPx().toInt(), - ), - alignment = Alignment.BottomStart, - onDismissRequest = { onDismissed.invoke() }, - properties = PopupProperties( - usePlatformDefaultWidth = false, - ), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .background( - shape = VideoTheme.shapes.sheet, - color = VideoTheme.colors.baseSheetPrimary, - ) - .padding(12.dp), - ) { - reactionsMenu() - Spacer(modifier = Modifier.size(VideoTheme.dimens.spacingM)) - - MenuEntry( - icon = R.drawable.stream_video_ic_screensharing, - label = "Toggle screen-sharing", - onClick = onScreenShareClick, - ) - MenuEntry( - vector = Icons.Default.AutoGraph, - icon = io.getstream.video.android.R.drawable.ic_layout_grid, - label = "Call stats", - onClick = onShowStates, - ) - MenuEntry( - icon = io.getstream.video.android.R.drawable.ic_mic, - label = "Switch Microphone", - onClick = onSwitchMicrophoneClick, - ) - 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 = onToggleBackgroundBlurClick, - ) - if (showDebugOptions) { - MenuEntry( - icon = R.drawable.stream_video_ic_fullscreen_exit, - label = "Toggle audio filter", - onClick = onToggleAudioFilterClick, - ) - MenuEntry( - icon = R.drawable.stream_video_ic_fullscreen_exit, - label = "Restart Subscriber Ice", - onClick = onRestartSubscriberIceClick, - ) - MenuEntry( - icon = R.drawable.stream_video_ic_fullscreen_exit, - label = "Restart Publisher Ice", - onClick = onRestartPublisherIceClick, - ) - MenuEntry( - icon = R.drawable.stream_video_ic_fullscreen_exit, - label = "Kill SFU WS", - onClick = onKillSfuWsClick, - ) - MenuEntry( - icon = R.drawable.stream_video_ic_fullscreen, - label = "Switch SFU", - onClick = onSwitchSfuClick, - ) - } - } - } -} - -@Composable -private fun MenuEntry( - vector: ImageVector? = null, - @DrawableRes icon: Int, - label: String, - onClick: () -> Unit, -) = StreamToggleButton( - onText = label, - offText = label, - onIcon = vector ?: ImageVector.vectorResource(icon), - onStyle = VideoTheme.styles.buttonStyles.toggleButtonStyleOn(StyleSize.XS).copy( - iconStyle = VideoTheme.styles.iconStyles.customColorIconStyle( - color = VideoTheme.colors.basePrimary, - ), - ), -) { onClick() } - -@Preview -@Composable -private fun SettingsPreview() { - StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) - VideoTheme { - SettingsMenuItems( - isBackgroundBlurEnabled = true, - onScreenShareClick = { }, - onSwitchMicrophoneClick = { }, - onToggleBackgroundBlurClick = { }, - onToggleAudioFilterClick = { }, - onRestartSubscriberIceClick = { }, - onRestartPublisherIceClick = { }, - onKillSfuWsClick = { }, - onSwitchSfuClick = { }, - showDebugOptions = true, - onDismissed = { }, - onShowStates = { }, - reactionsMenu = { - ReactionsMenu( - call = previewCall, - reactionMapper = ReactionMapper.defaultReactionMapper(), - ) { - } - }, - ) - } -} 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..db29fc29a3 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt @@ -0,0 +1,161 @@ +/* + * 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.ReadMore +import androidx.compose.material.icons.automirrored.filled.ScreenShare +import androidx.compose.material.icons.automirrored.filled.StopScreenShare +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.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.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.SubMenuItem + +fun defaultStreamMenu( + codecList: List, + onCodecSelected: (MediaCodecInfo) -> Unit, + isScreenShareEnabled: Boolean, + isBackgroundBlurEnabled: Boolean, + onSwitchMicrophoneClick: () -> Unit, + onToggleScreenShare: () -> Unit = {}, + onShowCallStats: () -> Unit, + onToggleBackgroundBlurClick: () -> Unit, + onToggleAudioFilterClick: () -> Unit, + onRestartSubscriberIceClick: () -> Unit, + onRestartPublisherIceClick: () -> Unit, + onKillSfuWsClick: () -> Unit, + onSwitchSfuClick: () -> Unit, + onDeviceSelected: (StreamAudioDevice) -> Unit, + availableDevices: List, +) = listOf( + ActionMenuItem( + title = if (isScreenShareEnabled) "Stop screen-share" else "Start screen-share", + icon = if (isScreenShareEnabled) Icons.AutoMirrored.Default.StopScreenShare else Icons.AutoMirrored.Default.ScreenShare, + action = onToggleScreenShare, + ), + ActionMenuItem( + title = "Call stats", + icon = Icons.Default.AutoGraph, + action = onShowCallStats, + ), + ActionMenuItem( + title = if (isBackgroundBlurEnabled) "Disable background blur" else "Enable background blur", + icon = if (isBackgroundBlurEnabled) Icons.Default.BlurOff else Icons.Default.BlurOn, + action = onToggleBackgroundBlurClick, + ), + SubMenuItem( + title = "Choose audio device", + icon = Icons.Default.SettingsVoice, + items = availableDevices.map { + val icon = when (it) { + is StreamAudioDevice.BluetoothHeadset -> Icons.Default.BluetoothAudio + is StreamAudioDevice.Earpiece -> Icons.Default.Headphones + is StreamAudioDevice.Speakerphone -> Icons.Default.SpeakerPhone + is StreamAudioDevice.WiredHeadset -> Icons.Default.HeadsetMic + } + ActionMenuItem( + title = it.name, + icon = icon, + action = { onDeviceSelected(it) }, + ) + }, + ), + SubMenuItem( + title = "Debug options", + icon = Icons.AutoMirrored.Default.ReadMore, + items = debugSubmenu( + codecList, + onCodecSelected, + onToggleAudioFilterClick, + onRestartSubscriberIceClick, + onRestartPublisherIceClick, + onKillSfuWsClick, + onSwitchSfuClick, + ), + ), +) + +fun codecMenu(codecList: List, onCodecSelected: (MediaCodecInfo) -> Unit) = + codecList.map { + val isHw = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + it.isHardwareAccelerated + } else { + false + } + ActionMenuItem( + title = it.name, + icon = Icons.Default.VideoFile, + highlight = isHw, + action = { onCodecSelected(it) }, + ) + } + +fun debugSubmenu( + codecList: List, + onCodecSelected: (MediaCodecInfo) -> Unit, + onToggleAudioFilterClick: () -> Unit, + onRestartSubscriberIceClick: () -> Unit, + onRestartPublisherIceClick: () -> Unit, + onKillSfuWsClick: () -> Unit, + onSwitchSfuClick: () -> Unit, +) = listOf( + SubMenuItem( + title = "Available video codecs", + icon = Icons.Default.VideoSettings, + items = codecMenu(codecList, onCodecSelected), + ), + ActionMenuItem( + title = "Toggle audio filter", + icon = Icons.Default.Audiotrack, + action = onToggleAudioFilterClick, + ), + ActionMenuItem( + title = "Restart subscriber Ice", + icon = Icons.Default.RestartAlt, + action = onRestartSubscriberIceClick, + ), + ActionMenuItem( + title = "Restart publisher Ice", + icon = Icons.Default.RestartAlt, + action = onRestartPublisherIceClick, + ), + ActionMenuItem( + title = "Shut down SFU web-socket", + icon = Icons.Default.PortableWifiOff, + action = onKillSfuWsClick, + ), + ActionMenuItem( + title = "Switch SFU", + icon = Icons.Default.SwitchLeft, + action = onSwitchSfuClick, + ), +) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt new file mode 100644 index 0000000000..76b3f07dfb --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt @@ -0,0 +1,234 @@ +/* + * 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.app.Activity +import android.graphics.Bitmap +import android.media.MediaCodecList +import android.media.projection.MediaProjectionManager +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.core.Call +import io.getstream.video.android.core.call.audio.AudioFilter +import io.getstream.video.android.core.call.video.BitmapVideoFilter +import io.getstream.video.android.core.mapper.ReactionMapper +import io.getstream.video.android.tooling.extensions.toPx +import io.getstream.video.android.ui.call.ReactionsMenu +import io.getstream.video.android.ui.menu.base.DynamicMenu +import io.getstream.video.android.util.BlurredBackgroundVideoFilter +import io.getstream.video.android.util.SampleAudioFilter +import kotlinx.coroutines.launch +import java.nio.ByteBuffer + +@OptIn(ExperimentalComposeUiApi::class) +@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 availableDevices by call.microphone.devices.collectAsStateWithLifecycle() + + val screenSharePermissionResult = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { + if (it.resultCode == Activity.RESULT_OK && it.data != null) { + call.startScreenSharing(it.data!!) + } + onDismissed.invoke() + }, + ) + + val isScreenSharing by call.screenShare.isEnabled.collectAsStateWithLifecycle() + val onScreenShareClick: () -> Unit = { + if (!isScreenSharing) { + scope.launch { + val mediaProjectionManager = + context.getSystemService(MediaProjectionManager::class.java) + screenSharePermissionResult.launch( + mediaProjectionManager.createScreenCaptureIntent(), + ) + } + } else { + call.stopScreenSharing() + } + } + val onSwitchMicrophoneClick: () -> Unit = { + onDismissed.invoke() + onDisplayAvailableDevice.invoke() + } + + val onToggleBackgroundBlurClick: () -> Unit = { + onToggleBackgroundBlur() + + if (call.videoFilter == null) { + call.videoFilter = object : BitmapVideoFilter() { + val filter = BlurredBackgroundVideoFilter() + + override fun filter(bitmap: Bitmap) { + filter.applyFilter(bitmap) + } + } + } else { + call.videoFilter = null + } + } + + val onToggleAudioFilterClick: () -> Unit = { + if (call.audioFilter == null) { + call.audioFilter = object : AudioFilter { + override fun filter( + audioFormat: Int, + channelCount: Int, + sampleRate: Int, + sampleData: ByteBuffer, + ) { + SampleAudioFilter.toRoboticVoice(sampleData, channelCount, 0.8f) + } + } + } else { + call.audioFilter = null + } + onDismissed() + } + + val onRestartSubscriberIceClick: () -> Unit = { + call.debug.restartSubscriberIce() + onDismissed.invoke() + Toast.makeText(context, "Restart Subscriber Ice", Toast.LENGTH_SHORT).show() + } + + val onRestartPublisherIceClick: () -> Unit = { + call.debug.restartPublisherIce() + onDismissed.invoke() + Toast.makeText(context, "Restart Publisher Ice", Toast.LENGTH_SHORT).show() + } + + val onKillSfuWsClick: () -> Unit = { + call.debug.doFullReconnection() + onDismissed.invoke() + Toast.makeText(context, "Killing SFU WS. Should trigger reconnect...", Toast.LENGTH_SHORT) + .show() + } + + val onSwitchSfuClick: () -> Unit = { + call.debug.switchSfu() + onDismissed.invoke() + Toast.makeText(context, "Switch sfu", Toast.LENGTH_SHORT).show() + } + + val codecInfos = remember { + MediaCodecList(MediaCodecList.ALL_CODECS).codecInfos.filter { + it.name.contains("encoder") && it.supportedTypes.firstOrNull { + it.contains("video") + } != null + } + } + Popup( + offset = IntOffset( + 0, + -(VideoTheme.dimens.componentHeightL + VideoTheme.dimens.spacingS).toPx().toInt(), + ), + alignment = Alignment.BottomStart, + onDismissRequest = { onDismissed.invoke() }, + properties = PopupProperties( + usePlatformDefaultWidth = false, + ), + ) { + DynamicMenu( + header = { + ReactionsMenu( + call = call, + reactionMapper = ReactionMapper.defaultReactionMapper(), + ) { + onDismissed() + } + }, + items = defaultStreamMenu( + codecList = codecInfos, + availableDevices = availableDevices, + onDeviceSelected = { + call.microphone.select(it) + onDismissed() + }, + onCodecSelected = { + onDismissed() + }, + onToggleScreenShare = onScreenShareClick, + onKillSfuWsClick = onKillSfuWsClick, + onRestartPublisherIceClick = onRestartPublisherIceClick, + onRestartSubscriberIceClick = onRestartSubscriberIceClick, + onSwitchMicrophoneClick = onSwitchMicrophoneClick, + onToggleAudioFilterClick = onToggleAudioFilterClick, + onToggleBackgroundBlurClick = onToggleBackgroundBlurClick, + onSwitchSfuClick = onSwitchSfuClick, + onShowCallStats = onShowCallStats, + isBackgroundBlurEnabled = isBackgroundBlurEnabled, + isScreenShareEnabled = isScreenSharing, + ), + ) + } +} + +@Preview +@Composable +private fun SettingsMenuPreview() { + VideoTheme { + DynamicMenu( + items = defaultStreamMenu( + codecList = emptyList(), + onCodecSelected = { + }, + isScreenShareEnabled = false, + isBackgroundBlurEnabled = true, + onSwitchMicrophoneClick = { }, + onToggleScreenShare = { }, + onShowCallStats = { }, + onToggleBackgroundBlurClick = { }, + onToggleAudioFilterClick = { }, + onRestartSubscriberIceClick = { }, + onRestartPublisherIceClick = { }, + onKillSfuWsClick = { }, + onSwitchSfuClick = { }, + availableDevices = emptyList(), + onDeviceSelected = { + }, + ), + ) + } +} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt new file mode 100644 index 0000000000..35852f2a36 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.ui.menu.base + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.getstream.video.android.compose.theme.base.VideoTheme +import io.getstream.video.android.compose.ui.components.base.StreamToggleButton +import io.getstream.video.android.compose.ui.components.base.styling.StyleSize +import io.getstream.video.android.ui.menu.debugSubmenu +import io.getstream.video.android.ui.menu.defaultStreamMenu + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun DynamicMenu(header: (@Composable LazyItemScope.() -> Unit)? = null, items: List) { + val history = remember { mutableStateListOf>>() } + Box( + modifier = Modifier + .fillMaxWidth() + .background( + color = VideoTheme.colors.baseSheetPrimary, + shape = VideoTheme.shapes.dialog, + ), + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .background( + shape = VideoTheme.shapes.sheet, + color = VideoTheme.colors.baseSheetPrimary, + ) + .padding(12.dp), + ) { + if (history.isEmpty()) { + header?.let { + item(content = header) + } + menuItems(items) { + history.add(Pair(it.title, it.items)) + } + } else { + val lastContent = history.last() + stickyHeader { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .background(VideoTheme.colors.baseSheetPrimary) + .fillMaxWidth(), + ) { + IconButton(onClick = { history.removeLastOrNull() }) { + Icon( + tint = VideoTheme.colors.basePrimary, + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = "Back", + ) + } + Text( + text = lastContent.first, + style = VideoTheme.typography.subtitleS, + color = VideoTheme.colors.basePrimary, + ) + } + } + + if (lastContent.second.isEmpty()) { + item { + Text( + textAlign = TextAlign.Center, + text = "No items", + style = VideoTheme.typography.subtitleS, + color = VideoTheme.colors.basePrimary, + ) + } + } else { + menuItems(lastContent.second) { + history.add(Pair(it.title, it.items)) + } + } + } + } + } +} + +private fun LazyListScope.menuItems(items: List, onNewSubmenu: (SubMenuItem) -> Unit) { + items(items.size) { index -> + val item = items[index] + val highlight = item.highlight + StreamToggleButton( + onText = item.title, + offText = item.title, + onIcon = item.icon, + onStyle = VideoTheme.styles.buttonStyles.toggleButtonStyleOn(StyleSize.XS).copy( + iconStyle = VideoTheme.styles.iconStyles.customColorIconStyle( + color = if (highlight) VideoTheme.colors.brandPrimary else VideoTheme.colors.basePrimary, + ), + ), + onClick = { + val actionItem = item as? ActionMenuItem + actionItem?.action?.invoke() + val menuItem = item as? SubMenuItem + menuItem?.let { + onNewSubmenu(it) + } + }, + ) + } +} + +@Preview +@Composable +private fun DynamicMenuPreview() { + VideoTheme { + DynamicMenu( + items = defaultStreamMenu( + codecList = emptyList(), + onCodecSelected = { + }, + isScreenShareEnabled = false, + isBackgroundBlurEnabled = true, + onSwitchMicrophoneClick = { }, + onToggleScreenShare = { }, + onShowCallStats = { }, + onToggleBackgroundBlurClick = { }, + onToggleAudioFilterClick = { }, + onRestartSubscriberIceClick = { }, + onRestartPublisherIceClick = { }, + onKillSfuWsClick = { }, + onSwitchSfuClick = { }, + availableDevices = emptyList(), + onDeviceSelected = { + }, + ), + ) + } +} + +@Preview +@Composable +private fun DynamicMenuDebugPreview() { + VideoTheme { + DynamicMenu( + items = debugSubmenu( + codecList = emptyList(), + onCodecSelected = { + }, + onKillSfuWsClick = { }, + onRestartPublisherIceClick = { }, + onRestartSubscriberIceClick = { }, + onToggleAudioFilterClick = { }, + onSwitchSfuClick = { }, + ), + ) + } +} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/MenuTypes.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/MenuTypes.kt new file mode 100644 index 0000000000..2f40738b5c --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/MenuTypes.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.ui.menu.base + +import androidx.compose.ui.graphics.vector.ImageVector + +open class MenuItem( + val title: String, + val icon: ImageVector, + val highlight: Boolean = false, +) + +class ActionMenuItem( + title: String, + icon: ImageVector, + highlight: Boolean = false, + val action: () -> Unit, +) : MenuItem(title, icon, highlight) + +class SubMenuItem(title: String, icon: ImageVector, val items: List) : + MenuItem(title, icon) From d3b76d344888b7fdaa9070114b76fdcb20ca1e03 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Tue, 13 Feb 2024 10:12:32 +0100 Subject: [PATCH 08/13] Do not show debug menu on release builds (#1012) --- .../video/android/ui/menu/MenuDefinitions.kt | 107 ++++++++++-------- .../video/android/ui/menu/SettingsMenu.kt | 7 +- .../video/android/ui/menu/base/DynamicMenu.kt | 29 ++++- 3 files changed, 89 insertions(+), 54 deletions(-) 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 index db29fc29a3..373e7cf58f 100644 --- 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 @@ -38,14 +38,15 @@ import androidx.compose.material.icons.filled.VideoFile 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.MenuItem import io.getstream.video.android.ui.menu.base.SubMenuItem fun defaultStreamMenu( + showDebugOptions: Boolean = false, codecList: List, onCodecSelected: (MediaCodecInfo) -> Unit, isScreenShareEnabled: Boolean, isBackgroundBlurEnabled: Boolean, - onSwitchMicrophoneClick: () -> Unit, onToggleScreenShare: () -> Unit = {}, onShowCallStats: () -> Unit, onToggleBackgroundBlurClick: () -> Unit, @@ -56,53 +57,65 @@ fun defaultStreamMenu( onSwitchSfuClick: () -> Unit, onDeviceSelected: (StreamAudioDevice) -> Unit, availableDevices: List, -) = listOf( - ActionMenuItem( - title = if (isScreenShareEnabled) "Stop screen-share" else "Start screen-share", - icon = if (isScreenShareEnabled) Icons.AutoMirrored.Default.StopScreenShare else Icons.AutoMirrored.Default.ScreenShare, - action = onToggleScreenShare, - ), - ActionMenuItem( - title = "Call stats", - icon = Icons.Default.AutoGraph, - action = onShowCallStats, - ), - ActionMenuItem( - title = if (isBackgroundBlurEnabled) "Disable background blur" else "Enable background blur", - icon = if (isBackgroundBlurEnabled) Icons.Default.BlurOff else Icons.Default.BlurOn, - action = onToggleBackgroundBlurClick, - ), - SubMenuItem( - title = "Choose audio device", - icon = Icons.Default.SettingsVoice, - items = availableDevices.map { - val icon = when (it) { - is StreamAudioDevice.BluetoothHeadset -> Icons.Default.BluetoothAudio - is StreamAudioDevice.Earpiece -> Icons.Default.Headphones - is StreamAudioDevice.Speakerphone -> Icons.Default.SpeakerPhone - is StreamAudioDevice.WiredHeadset -> Icons.Default.HeadsetMic - } - ActionMenuItem( - title = it.name, - icon = icon, - action = { onDeviceSelected(it) }, - ) - }, - ), - SubMenuItem( - title = "Debug options", - icon = Icons.AutoMirrored.Default.ReadMore, - items = debugSubmenu( - codecList, - onCodecSelected, - onToggleAudioFilterClick, - onRestartSubscriberIceClick, - onRestartPublisherIceClick, - onKillSfuWsClick, - onSwitchSfuClick, +) = buildList { + add( + ActionMenuItem( + title = if (isScreenShareEnabled) "Stop screen-share" else "Start screen-share", + icon = if (isScreenShareEnabled) Icons.AutoMirrored.Default.StopScreenShare else Icons.AutoMirrored.Default.ScreenShare, + action = onToggleScreenShare, ), - ), -) + ) + add( + ActionMenuItem( + title = "Call stats", + icon = Icons.Default.AutoGraph, + action = onShowCallStats, + ), + ) + add( + ActionMenuItem( + title = if (isBackgroundBlurEnabled) "Disable background blur" else "Enable background blur", + icon = if (isBackgroundBlurEnabled) Icons.Default.BlurOff else Icons.Default.BlurOn, + action = onToggleBackgroundBlurClick, + ), + ) + add( + SubMenuItem( + title = "Choose audio device", + icon = Icons.Default.SettingsVoice, + items = availableDevices.map { + val icon = when (it) { + is StreamAudioDevice.BluetoothHeadset -> Icons.Default.BluetoothAudio + is StreamAudioDevice.Earpiece -> Icons.Default.Headphones + is StreamAudioDevice.Speakerphone -> Icons.Default.SpeakerPhone + is StreamAudioDevice.WiredHeadset -> Icons.Default.HeadsetMic + } + ActionMenuItem( + title = it.name, + icon = icon, + action = { onDeviceSelected(it) }, + ) + }, + ), + ) + if (showDebugOptions) { + add( + SubMenuItem( + title = "Debug options", + icon = Icons.AutoMirrored.Default.ReadMore, + items = debugSubmenu( + codecList, + onCodecSelected, + onToggleAudioFilterClick, + onRestartSubscriberIceClick, + onRestartPublisherIceClick, + onKillSfuWsClick, + onSwitchSfuClick, + ), + ), + ) + } +} fun codecMenu(codecList: List, onCodecSelected: (MediaCodecInfo) -> Unit) = codecList.map { diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt index 76b3f07dfb..526bcd97d9 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt @@ -88,10 +88,6 @@ internal fun SettingsMenu( call.stopScreenSharing() } } - val onSwitchMicrophoneClick: () -> Unit = { - onDismissed.invoke() - onDisplayAvailableDevice.invoke() - } val onToggleBackgroundBlurClick: () -> Unit = { onToggleBackgroundBlur() @@ -180,6 +176,7 @@ internal fun SettingsMenu( } }, items = defaultStreamMenu( + showDebugOptions = showDebugOptions, codecList = codecInfos, availableDevices = availableDevices, onDeviceSelected = { @@ -193,7 +190,6 @@ internal fun SettingsMenu( onKillSfuWsClick = onKillSfuWsClick, onRestartPublisherIceClick = onRestartPublisherIceClick, onRestartSubscriberIceClick = onRestartSubscriberIceClick, - onSwitchMicrophoneClick = onSwitchMicrophoneClick, onToggleAudioFilterClick = onToggleAudioFilterClick, onToggleBackgroundBlurClick = onToggleBackgroundBlurClick, onSwitchSfuClick = onSwitchSfuClick, @@ -216,7 +212,6 @@ private fun SettingsMenuPreview() { }, isScreenShareEnabled = false, isBackgroundBlurEnabled = true, - onSwitchMicrophoneClick = { }, onToggleScreenShare = { }, onShowCallStats = { }, onToggleBackgroundBlurClick = { }, diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt index 35852f2a36..93598e9548 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt @@ -151,7 +151,34 @@ private fun DynamicMenuPreview() { }, isScreenShareEnabled = false, isBackgroundBlurEnabled = true, - onSwitchMicrophoneClick = { }, + onToggleScreenShare = { }, + onShowCallStats = { }, + onToggleBackgroundBlurClick = { }, + onToggleAudioFilterClick = { }, + onRestartSubscriberIceClick = { }, + onRestartPublisherIceClick = { }, + onKillSfuWsClick = { }, + onSwitchSfuClick = { }, + availableDevices = emptyList(), + onDeviceSelected = { + }, + ), + ) + } +} + +@Preview +@Composable +private fun DynamicMenuDebugOptionPreview() { + VideoTheme { + DynamicMenu( + items = defaultStreamMenu( + showDebugOptions = true, + codecList = emptyList(), + onCodecSelected = { + }, + isScreenShareEnabled = true, + isBackgroundBlurEnabled = true, onToggleScreenShare = { }, onShowCallStats = { }, onToggleBackgroundBlurClick = { }, From 42e6664ac33b3285b2a98728e5d48998f868fac1 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Thu, 15 Feb 2024 13:19:16 +0100 Subject: [PATCH 09/13] Recordings library and dynamic loading sub-menus (#1013) --- demo-app/src/main/AndroidManifest.xml | 2 + .../video/android/ui/menu/MenuDefinitions.kt | 19 ++++ .../video/android/ui/menu/SettingsMenu.kt | 77 ++++++++++++- .../video/android/ui/menu/base/DynamicMenu.kt | 106 ++++++++++++++---- .../video/android/ui/menu/base/MenuTypes.kt | 34 +++++- .../api/stream-video-android-core.api | 3 +- .../io/getstream/video/android/core/Call.kt | 9 +- .../video/android/core/StreamVideoImpl.kt | 8 +- 8 files changed, 227 insertions(+), 31 deletions(-) diff --git a/demo-app/src/main/AndroidManifest.xml b/demo-app/src/main/AndroidManifest.xml index bb41e838a9..353250d768 100644 --- a/demo-app/src/main/AndroidManifest.xml +++ b/demo-app/src/main/AndroidManifest.xml @@ -18,6 +18,8 @@ xmlns:tools="http://schemas.android.com/tools"> + + 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 index 373e7cf58f..50a85bbc1d 100644 --- 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 @@ -35,12 +35,17 @@ 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, @@ -57,6 +62,7 @@ fun defaultStreamMenu( onSwitchSfuClick: () -> Unit, onDeviceSelected: (StreamAudioDevice) -> Unit, availableDevices: List, + loadRecordings: suspend () -> List, ) = buildList { add( ActionMenuItem( @@ -98,6 +104,13 @@ fun defaultStreamMenu( }, ), ) + add( + DynamicSubMenuItem( + title = "Recordings", + icon = Icons.Default.VideoLibrary, + itemsLoader = loadRecordings, + ), + ) if (showDebugOptions) { add( SubMenuItem( @@ -117,6 +130,9 @@ fun defaultStreamMenu( } } +/** + * Lists the available codecs for this device as list of [MenuItem] + */ fun codecMenu(codecList: List, onCodecSelected: (MediaCodecInfo) -> Unit) = codecList.map { val isHw = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -132,6 +148,9 @@ fun codecMenu(codecList: List, onCodecSelected: (MediaCodecInfo) ) } +/** + * Optionally defines the debug sub-menu of the demo app. + */ fun debugSubmenu( codecList: List, onCodecSelected: (MediaCodecInfo) -> Unit, diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt index 526bcd97d9..5f7307c23d 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt @@ -16,14 +16,24 @@ package io.getstream.video.android.ui.menu +import android.Manifest import android.app.Activity +import android.app.DownloadManager +import android.content.Context +import android.content.Context.DOWNLOAD_SERVICE import android.graphics.Bitmap import android.media.MediaCodecList import android.media.projection.MediaProjectionManager +import android.net.Uri +import android.os.Build +import android.os.Environment import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.VideoFile import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -34,7 +44,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties +import androidx.core.content.ContextCompat.getSystemService import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionStatus +import com.google.accompanist.permissions.rememberPermissionState import io.getstream.video.android.compose.theme.base.VideoTheme import io.getstream.video.android.core.Call import io.getstream.video.android.core.call.audio.AudioFilter @@ -42,13 +56,15 @@ import io.getstream.video.android.core.call.video.BitmapVideoFilter import io.getstream.video.android.core.mapper.ReactionMapper import io.getstream.video.android.tooling.extensions.toPx import io.getstream.video.android.ui.call.ReactionsMenu +import io.getstream.video.android.ui.menu.base.ActionMenuItem import io.getstream.video.android.ui.menu.base.DynamicMenu +import io.getstream.video.android.ui.menu.base.MenuItem import io.getstream.video.android.util.BlurredBackgroundVideoFilter import io.getstream.video.android.util.SampleAudioFilter import kotlinx.coroutines.launch import java.nio.ByteBuffer -@OptIn(ExperimentalComposeUiApi::class) +@OptIn(ExperimentalComposeUiApi::class, ExperimentalPermissionsApi::class) @Composable internal fun SettingsMenu( call: Call, @@ -155,6 +171,29 @@ internal fun SettingsMenu( } != null } } + + val onLoadRecordings: suspend () -> List = storagePermissionAndroidBellow10 { + when (it) { + is PermissionStatus.Granted -> { + { + call.listRecordings().getOrNull()?.recordings?.map { + ActionMenuItem( + title = it.filename, + icon = Icons.Default.VideoFile, + action = { + context.downloadFile(it.url, it.filename) + onDismissed() + }, + ) + } ?: emptyList() + } + } + is PermissionStatus.Denied -> { + { emptyList() } + } + } + } + Popup( offset = IntOffset( 0, @@ -196,11 +235,46 @@ internal fun SettingsMenu( onShowCallStats = onShowCallStats, isBackgroundBlurEnabled = isBackgroundBlurEnabled, isScreenShareEnabled = isScreenSharing, + loadRecordings = onLoadRecordings, ), ) } } +@OptIn(ExperimentalPermissionsApi::class) +@Composable +private fun storagePermissionAndroidBellow10( + permission: (PermissionStatus) -> suspend () -> List, +): suspend () -> List { + // Check if the device's API level is below Android 10 (API level 29) + return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + val writeStoragePermissionState = + rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) + LaunchedEffect(key1 = true) { + // Request permission + writeStoragePermissionState.launchPermissionRequest() + } + permission(writeStoragePermissionState.status) + } else { + permission(PermissionStatus.Granted) + } +} + +private fun Context.downloadFile(url: String, title: String) { + val request = DownloadManager.Request(Uri.parse(url)) + .setTitle(title) // Title of the Download Notification + .setDescription("Downloading") // Description of the Download Notification + .setNotificationVisibility( + DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED, + ) // Visibility of the download Notification + .setAllowedOverMetered(true) // Set if download is allowed on Mobile network + .setAllowedOverRoaming(true) // Set if download is allowed on Roaming network + .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, title) + + val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager + downloadManager.enqueue(request) // enqueue puts the download request in the queue. +} + @Preview @Composable private fun SettingsMenuPreview() { @@ -223,6 +297,7 @@ private fun SettingsMenuPreview() { availableDevices = emptyList(), onDeviceSelected = { }, + loadRecordings = { emptyList() }, ), ) } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt index 93598e9548..2f31ba0344 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt @@ -27,12 +27,17 @@ import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign @@ -44,10 +49,21 @@ import io.getstream.video.android.compose.ui.components.base.styling.StyleSize import io.getstream.video.android.ui.menu.debugSubmenu import io.getstream.video.android.ui.menu.defaultStreamMenu +/** + * A composable capable of loading a menu based on a list structure of menu items and sub menus. + * There are three types of items: + * - [ActionMenuItem] - shown normally as an item that can be clicked. + * - [SubMenuItem] - that contains another list of [ActionMenuItem] o [SubMenuItem] which will be shown when clicked. + * - [DynamicSubMenuItem] - that shows a spinner and calls a loading function before behaving as [SubMenuItem] + * + * The transition and history between the items is automatic. + */ @OptIn(ExperimentalFoundationApi::class) @Composable fun DynamicMenu(header: (@Composable LazyItemScope.() -> Unit)? = null, items: List) { - val history = remember { mutableStateListOf>>() } + val history = remember { mutableStateListOf>() } + val dynamicItems = remember { mutableStateListOf() } + var loadedItems by remember { mutableStateOf(false) } Box( modifier = Modifier .fillMaxWidth() @@ -70,7 +86,7 @@ fun DynamicMenu(header: (@Composable LazyItemScope.() -> Unit)? = null, items: L item(content = header) } menuItems(items) { - history.add(Pair(it.title, it.items)) + history.add(Pair(it.title, it)) } } else { val lastContent = history.last() @@ -96,18 +112,31 @@ fun DynamicMenu(header: (@Composable LazyItemScope.() -> Unit)? = null, items: L } } - if (lastContent.second.isEmpty()) { - item { - Text( - textAlign = TextAlign.Center, - text = "No items", - style = VideoTheme.typography.subtitleS, - color = VideoTheme.colors.basePrimary, - ) + val subMenu = lastContent.second + val dynamicMenu = subMenu as? DynamicSubMenuItem + + if (dynamicMenu != null) { + if (!loadedItems) { + dynamicItems.clear() + loadingItems(dynamicMenu) { + loadedItems = true + dynamicItems.addAll(it) + } + } + if (dynamicItems.isNotEmpty()) { + menuItems(dynamicItems) { + history.add(Pair(it.title, it)) + } + } else if (loadedItems) { + noItems() } } else { - menuItems(lastContent.second) { - history.add(Pair(it.title, it.items)) + if (subMenu.items.isEmpty()) { + noItems() + } else { + menuItems(subMenu.items) { + history.add(Pair(it.title, it)) + } } } } @@ -115,7 +144,41 @@ fun DynamicMenu(header: (@Composable LazyItemScope.() -> Unit)? = null, items: L } } -private fun LazyListScope.menuItems(items: List, onNewSubmenu: (SubMenuItem) -> Unit) { +private fun LazyListScope.loadingItems( + dynamicMenu: DynamicSubMenuItem, + onLoaded: (List) -> Unit, +) { + item { + LaunchedEffect(key1 = dynamicMenu) { + onLoaded(dynamicMenu.itemsLoader.invoke()) + } + LinearProgressIndicator( + modifier = Modifier + .padding(33.dp) + .fillMaxWidth(), + color = VideoTheme.colors.basePrimary, + ) + } +} + +private fun LazyListScope.noItems() { + item { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + textAlign = TextAlign.Center, + text = "No items", + style = VideoTheme.typography.subtitleS, + color = VideoTheme.colors.basePrimary, + ) + } +} + +private fun LazyListScope.menuItems( + items: List, + onNewSubmenu: (SubMenuItem) -> Unit, +) { items(items.size) { index -> val item = items[index] val highlight = item.highlight @@ -147,8 +210,7 @@ private fun DynamicMenuPreview() { DynamicMenu( items = defaultStreamMenu( codecList = emptyList(), - onCodecSelected = { - }, + onCodecSelected = {}, isScreenShareEnabled = false, isBackgroundBlurEnabled = true, onToggleScreenShare = { }, @@ -160,8 +222,8 @@ private fun DynamicMenuPreview() { onKillSfuWsClick = { }, onSwitchSfuClick = { }, availableDevices = emptyList(), - onDeviceSelected = { - }, + onDeviceSelected = {}, + loadRecordings = { emptyList() }, ), ) } @@ -175,8 +237,7 @@ private fun DynamicMenuDebugOptionPreview() { items = defaultStreamMenu( showDebugOptions = true, codecList = emptyList(), - onCodecSelected = { - }, + onCodecSelected = {}, isScreenShareEnabled = true, isBackgroundBlurEnabled = true, onToggleScreenShare = { }, @@ -188,8 +249,8 @@ private fun DynamicMenuDebugOptionPreview() { onKillSfuWsClick = { }, onSwitchSfuClick = { }, availableDevices = emptyList(), - onDeviceSelected = { - }, + onDeviceSelected = {}, + loadRecordings = { emptyList() }, ), ) } @@ -202,8 +263,7 @@ private fun DynamicMenuDebugPreview() { DynamicMenu( items = debugSubmenu( codecList = emptyList(), - onCodecSelected = { - }, + onCodecSelected = {}, onKillSfuWsClick = { }, onRestartPublisherIceClick = { }, onRestartSubscriberIceClick = { }, diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/MenuTypes.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/MenuTypes.kt index 2f40738b5c..21e72d695f 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/MenuTypes.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/MenuTypes.kt @@ -18,12 +18,24 @@ package io.getstream.video.android.ui.menu.base import androidx.compose.ui.graphics.vector.ImageVector -open class MenuItem( +/** + * Parent class on all menu items. + * + * @param title - title of the item, used to display in the menu, or a subtitle to the sub menu. + * @param icon - the icon to be shown with the item. + * @param highlight - if the icon should be highlighted or not (usually tinted with primary color) + */ +abstract class MenuItem( val title: String, val icon: ImageVector, val highlight: Boolean = false, ) +/** + * Same as [MenuItem] but additionally has an action associated with it. + * + * @param action - the action that will execute when the item is clicked. + */ class ActionMenuItem( title: String, icon: ImageVector, @@ -31,5 +43,23 @@ class ActionMenuItem( val action: () -> Unit, ) : MenuItem(title, icon, highlight) -class SubMenuItem(title: String, icon: ImageVector, val items: List) : +/** + * Unlike the [ActionMenuItem] the [SubMenuItem] contains a list of [MenuItem] that create a new submenu. + * Clicking a [SubMenuItem] will show the [items]. + * + * @param items - the items will be shown in the menu. + */ +open class SubMenuItem(title: String, icon: ImageVector, val items: List) : MenuItem(title, icon) + +/** + * Similar to the [SubMenuItem] the [DynamicSubMenuItem] contains an [itemsLoader] function to load the items. + * The [DynamicMenu] knows how to invoke this function to dynamically load the items while showing a progress indicator. + * + * @param itemsLoader the items provider function. + */ +class DynamicSubMenuItem( + title: String, + icon: ImageVector, + val itemsLoader: suspend () -> List, +) : SubMenuItem(title, icon, emptyList()) diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index 04e2352a83..4f92200699 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -37,7 +37,8 @@ public final class io/getstream/video/android/core/Call { public final fun join (ZLio/getstream/video/android/core/CreateCallOptions;ZZLkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun join$default (Lio/getstream/video/android/core/Call;ZLio/getstream/video/android/core/CreateCallOptions;ZZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun leave ()V - public final fun listRecordings (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun listRecordings (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listRecordings$default (Lio/getstream/video/android/core/Call;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun muteAllUsers (ZZZLkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun muteAllUsers$default (Lio/getstream/video/android/core/Call;ZZZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun muteUser (Ljava/lang/String;ZZZLkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt index 4a277806d7..3cb4b64843 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt @@ -916,8 +916,13 @@ public class Call( } } - suspend fun listRecordings(): Result { - return clientImpl.listRecordings(type, id, "what") + /** + * List the recordings for this call. + * + * @param sessionId - if session ID is supplied, only recordings for that session will be loaded. + */ + suspend fun listRecordings(sessionId: String? = null): Result { + return clientImpl.listRecordings(type, id, sessionId) } suspend fun muteUser( diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt index 2cab12bf03..3be370d569 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt @@ -931,11 +931,15 @@ internal class StreamVideoImpl internal constructor( suspend fun listRecordings( type: String, id: String, - sessionId: String, + sessionId: String?, ): Result { return wrapAPICall { val result = - connectionModule.api.listRecordingsTypeIdSession1(type, id, sessionId) + if (sessionId == null) { + connectionModule.api.listRecordingsTypeId0(type, id) + } else { + connectionModule.api.listRecordingsTypeIdSession1(type, id, sessionId) + } result } } From edd849008ff64bfb250055858c18b0f059047879 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 16 Feb 2024 16:14:34 +0100 Subject: [PATCH 10/13] Add feedback dialog and menu items along with simple HTTP post sender to send the feedback (#1014) --- demo-app/build.gradle.kts | 3 + .../video/android/ui/call/CallScreen.kt | 26 ++- .../video/android/ui/call/FeedbackDialog.kt | 209 ++++++++++++++++++ .../video/android/ui/menu/MenuDefinitions.kt | 28 ++- .../video/android/ui/menu/SettingsMenu.kt | 5 +- .../video/android/ui/menu/base/DynamicMenu.kt | 2 + .../video/android/util/FeedbackSender.kt | 74 +++++++ .../main/res/drawable/feedback_artwork.png | Bin 0 -> 254088 bytes 8 files changed, 323 insertions(+), 24 deletions(-) create mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/ui/call/FeedbackDialog.kt create mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/util/FeedbackSender.kt create mode 100644 demo-app/src/main/res/drawable/feedback_artwork.png diff --git a/demo-app/build.gradle.kts b/demo-app/build.gradle.kts index 7e97324582..4ba5e8daf0 100644 --- a/demo-app/build.gradle.kts +++ b/demo-app/build.gradle.kts @@ -269,6 +269,9 @@ dependencies { implementation(libs.google.mlkit.selfie.segmentation) implementation(files("libs/renderscript-toolkit.aar")) + // Http + implementation(libs.okhttp) + // Memory detection debugImplementation(libs.leakCanary) 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 5e05efd7fe..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 @@ -121,6 +121,7 @@ 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) } @@ -446,25 +447,26 @@ fun CallScreen( 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 }, - ) { - isShowingStats = true - isShowingSettingMenu = false - } + 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) { 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/menu/MenuDefinitions.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt index 50a85bbc1d..54a20bf6ed 100644 --- 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 @@ -19,14 +19,14 @@ 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.automirrored.filled.ScreenShare -import androidx.compose.material.icons.automirrored.filled.StopScreenShare 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 @@ -60,15 +60,16 @@ fun defaultStreamMenu( onRestartPublisherIceClick: () -> Unit, onKillSfuWsClick: () -> Unit, onSwitchSfuClick: () -> Unit, + onShowFeedback: () -> Unit, onDeviceSelected: (StreamAudioDevice) -> Unit, availableDevices: List, loadRecordings: suspend () -> List, ) = buildList { add( - ActionMenuItem( - title = if (isScreenShareEnabled) "Stop screen-share" else "Start screen-share", - icon = if (isScreenShareEnabled) Icons.AutoMirrored.Default.StopScreenShare else Icons.AutoMirrored.Default.ScreenShare, - action = onToggleScreenShare, + DynamicSubMenuItem( + title = "Recordings", + icon = Icons.Default.VideoLibrary, + itemsLoader = loadRecordings, ), ) add( @@ -105,10 +106,17 @@ fun defaultStreamMenu( ), ) add( - DynamicSubMenuItem( - title = "Recordings", - icon = Icons.Default.VideoLibrary, - itemsLoader = loadRecordings, + ActionMenuItem( + title = "Feedback", + icon = Icons.Default.Feedback, + action = onShowFeedback, + ), + ) + add( + ActionMenuItem( + title = if (isScreenShareEnabled) "Stop screen-share" else "Start screen-share", + icon = Icons.AutoMirrored.Default.MobileScreenShare, + action = onToggleScreenShare, ), ) if (showDebugOptions) { diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt index 5f7307c23d..8d43d2fa49 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt @@ -70,9 +70,8 @@ internal fun SettingsMenu( call: Call, showDebugOptions: Boolean, isBackgroundBlurEnabled: Boolean, - onDisplayAvailableDevice: () -> Unit, onDismissed: () -> Unit, - onShowReactionsMenu: () -> Unit, + onShowFeedback: () -> Unit, onToggleBackgroundBlur: () -> Unit, onShowCallStats: () -> Unit, ) { @@ -225,6 +224,7 @@ internal fun SettingsMenu( onCodecSelected = { onDismissed() }, + onShowFeedback = onShowFeedback, onToggleScreenShare = onScreenShareClick, onKillSfuWsClick = onKillSfuWsClick, onRestartPublisherIceClick = onRestartPublisherIceClick, @@ -297,6 +297,7 @@ private fun SettingsMenuPreview() { availableDevices = emptyList(), onDeviceSelected = { }, + onShowFeedback = {}, loadRecordings = { emptyList() }, ), ) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt index 2f31ba0344..4858917964 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt @@ -223,6 +223,7 @@ private fun DynamicMenuPreview() { onSwitchSfuClick = { }, availableDevices = emptyList(), onDeviceSelected = {}, + onShowFeedback = {}, loadRecordings = { emptyList() }, ), ) @@ -250,6 +251,7 @@ private fun DynamicMenuDebugOptionPreview() { onSwitchSfuClick = { }, availableDevices = emptyList(), onDeviceSelected = {}, + onShowFeedback = {}, loadRecordings = { emptyList() }, ), ) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/FeedbackSender.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/FeedbackSender.kt new file mode 100644 index 0000000000..a4b6383d72 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/FeedbackSender.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.util + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import java.net.URL + +/** + * A simple http post sender for the feedback request. + */ +class FeedbackSender { + private val client = OkHttpClient() + + fun isValidEmail(email: String): Boolean { + val emailRegex = "^[A-Za-z](.*)([@]{1})(.{1,})(\\.)(.{1,})".toRegex() + return email.matches(emailRegex) + } + + fun sendFeedback(email: String, message: String, callId: String, coroutineScope: CoroutineScope = MainScope(), onFinished: (isError: Boolean) -> Unit) { + coroutineScope.launch(Dispatchers.IO) { + val error = try { + val response = sendFeedbackInternal(email, message, callId) + when (response.code) { + 204 -> false + else -> true + } + } catch (e: Exception) { + true + } + onFinished(error) + } + } + + private fun sendFeedbackInternal(email: String, message: String, callId: String): Response { + val url = URL("https://getstream.io/api/crm/video_feedback/") + val formData = MultipartBody.Builder().apply { + addFormDataPart("email", email) + addFormDataPart("message", message) + addFormDataPart("page_url", "https://www.getstream.io?meeting=true&id=$callId") + setType(MultipartBody.FORM) + } + val request = Request.Builder() + .url(url) + .post(formData.build()) + .header("User-Agent", "StreamDemoApp-Android/1.0.0") + .header("Connection", "keep-alive") + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .build() + + return client.newCall(request).execute() + } +} diff --git a/demo-app/src/main/res/drawable/feedback_artwork.png b/demo-app/src/main/res/drawable/feedback_artwork.png new file mode 100644 index 0000000000000000000000000000000000000000..995dedb26d3090f3d9defa359922816553721736 GIT binary patch literal 254088 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%0DiiV9&| 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 literal 0 HcmV?d00001 From 496521ad8b2f5d8faa1dc24123358e8ac783d175 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Wed, 21 Feb 2024 17:01:23 +0100 Subject: [PATCH 11/13] Update docs on Intent filters and incoming/outgoing call requirements (#1016) --- .../docs/Android/01-basics/03-quickstart.mdx | 52 +++++++++++++++++++ .../02-push-notifications/02-setup.mdx | 23 +++++--- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/docusaurus/docs/Android/01-basics/03-quickstart.mdx b/docusaurus/docs/Android/01-basics/03-quickstart.mdx index ef58d92998..974e5ad5a1 100644 --- a/docusaurus/docs/Android/01-basics/03-quickstart.mdx +++ b/docusaurus/docs/Android/01-basics/03-quickstart.mdx @@ -31,6 +31,58 @@ The call type controls the permissions and which features are enabled. The second argument is the call id and is optional. It's convenient to specify an ID if the call is associated with an object in your database. As an example if you're building a ride sharing app like Uber, you could use the ride id as the call id to easily align with your internal systems. +#### Incoming / outgoing calls +If you intend to support incoming and outgoing calls the SDK must know which activities to call in the notification `PendingIntent`. +In order to be able to accept and send incoming calls via the default notification handler provided by the SDK, you need to handle the intent actions in your manifest. + +The SDK defines the following actions: +```kotlin +ACTION_NOTIFICATION = "io.getstream.video.android.action.NOTIFICATION" +ACTION_LIVE_CALL = "io.getstream.video.android.action.LIVE_CALL" +ACTION_INCOMING_CALL = "io.getstream.video.android.action.INCOMING_CALL" +ACTION_OUTGOING_CALL = "io.getstream.video.android.action.OUTGOING_CALL" +ACTION_ACCEPT_CALL = "io.getstream.video.android.action.ACCEPT_CALL" +ACTION_REJECT_CALL = "io.getstream.video.android.action.REJECT_CALL" +ACTION_LEAVE_CALL = "io.getstream.video.android.action.LEAVE_CALL" +ACTION_ONGOING_CALL = "io.getstream.video.android.action.ONGOING_CALL" +``` + +If you do not support incoming and outgoing calls, you can skip the `` declarations. + +In order to be able to fully utilize the incoming / outgoing feature the SDK needs to know which activity these actions resolve to in order to construct the `PendingIntent`s. +You have to provide this information into your manifest. + +The `ACTION_REJECT_CALL` and `ACTION_LEAVE_CALL` are handled by default by the SDK and you do not have to do anything about them. +The `ACTION_ONGOING_CALL` does not mandate an `` with the consequence that omitting this will result in reduced functionality where the user will not be returned to your app if the notification is clicked. + +All the other actions must be declared in your manifest, otherwise the internal `CallService` will fail to create the required notification for a foreground service and thus not start, resulting in an exception. + +```xml + + + + + + + + + + + + + +``` +:::info +You can handle multiple `IntentFilter` within a single `activity` if you prefer or have separate activity for each action. +::: + +For more details on notification customization see our [Push Notification Guide](../06-advanced/02-push-notifications/01-overview.mdx). + ### Rendering video The call's state is available in [`call.state`](../03-guides/03-call-and-participant-state.mdx) and you'll often work with `call.state.participants`. diff --git a/docusaurus/docs/Android/06-advanced/02-push-notifications/02-setup.mdx b/docusaurus/docs/Android/06-advanced/02-push-notifications/02-setup.mdx index 41259463d5..be1e3051ae 100644 --- a/docusaurus/docs/Android/06-advanced/02-push-notifications/02-setup.mdx +++ b/docusaurus/docs/Android/06-advanced/02-push-notifications/02-setup.mdx @@ -66,28 +66,36 @@ You only need to create the `Activity`/`Activities` will be called when the Push The different actions we provide are: * `io.getstream.video.android.action.INCOMING_CALL`: Action used to process an incoming call. The `activity` that handles this action should show options to accept/reject the call. You can use our [RigningCallContent](../../04-ui-components/04-call/04-ringing-call.mdx) component to build your screen. This screen can be shown when the device is locked, by adding some arguments on the manifest. +* `io.getstream.video.android.action.OUTGOING_CALL`: Action used to process an outgoing call. The `activity` that handles this action should show options to cancel the call. You can use our [RigningCallContent](../../04-ui-components/04-call/04-ringing-call.mdx) component to build your screen. * `io.getstream.video.android.action.ACCEPT_CALL`: Action used to accept an incoming call. The `activity` that handles this action should accept the call and show the call screen. You can use our [CallContent](../../04-ui-components/04-call/03-call-content.mdx) component to build your screen. * `io.getstream.video.android.action.LIVE_CALL`: Action used to go into a live call. The `activity` that handles this action should show the live call screen. You can use our [CallContent](../../04-ui-components/04-call/03-call-content.mdx) component to build your screen. * `io.getstream.video.android.action.ONGOING_CALL`: Action used to get back into already running call. The `activity` that handles this action should show the call screen. You can use our [CallContent](../../04-ui-components/04-call/03-call-content.mdx) component to build your screen. These actions need to be included on the `AndroidManifest` while you declare your activities: -``` xml {4,9-11,15,18-20,24,27-29,33,36-38} +``` xml {4,8-10,13,17-19,23,26-28,31,33-35,39,42-44} + android:showWhenLocked="true"> + + + + + + android:exported="false"> @@ -95,8 +103,7 @@ These actions need to be included on the `AndroidManifest` while you declare you + android:exported="false"> @@ -142,7 +149,7 @@ class MyNotificationHandler(private val context: Context) : NotificationHandler context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } - // Called when a Ringing Call arrives. + // Called when a Ringing Call arrives. (outgoing or incoming) override fun onRingingCall(callId: StreamCallId, callDisplayName: String) { val notification = NotificationCompat.Builder(context, notificationChannelId) ... // Configure your own notification From c8a7c89ca9148fa55f01c2ef359790ffb8e4652a Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Fri, 23 Feb 2024 10:22:39 +0900 Subject: [PATCH 12/13] Bump Compose 1.6.2 and Compiler to 1.5.10, and update other dependencies (#1017) --- gradle/libs.versions.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 12c8376c38..331aa1ce9d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ kotlin = "1.9.22" ksp = "1.9.22-1.0.17" kotlinSerialization = "1.6.2" kotlinSerializationConverter = "1.0.0" -kotlinxCoroutines = "1.7.3" +kotlinxCoroutines = "1.8.0" kotlinDokka = "1.9.10" jvmTarget = "11" @@ -22,15 +22,15 @@ androidxActivity = "1.8.0" androidxDataStore = "1.0.0" googleService = "4.3.14" -androidxComposeBom = "2024.01.00" -androidxComposeCompiler = "1.5.8" +androidxComposeBom = "2024.02.00" +androidxComposeCompiler = "1.5.10" androidxComposeTracing = "1.0.0-beta01" androidxHiltNavigation = "1.1.0" androidxComposeNavigation = "2.7.6" composeStableMarker = "1.0.2" coil = "2.5.0" -landscapist = "2.3.0" +landscapist = "2.3.1" accompanist = "0.32.0" telephoto = "0.3.0" audioswitch = "1.1.8" @@ -54,7 +54,7 @@ androidxTest = "1.5.2" androidxTestCore = "1.5.0" androidxProfileinstaller = "1.3.1" androidxMacroBenchmark = "1.2.3" -androidxUiAutomator = "2.3.0-beta01" +androidxUiAutomator = "2.3.0" androidxContraintLayout = "2.1.4" androidxEspresso = "3.5.1" androidxJunit = "1.1.5" From b98ed7989c0e8ec2ec4cdae12f73cfa9a63c3170 Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Mon, 26 Feb 2024 18:19:51 +0900 Subject: [PATCH 13/13] Fix unsynced terms on the introduction page (#1018) --- docusaurus/docs/Android/01-basics/01-introduction.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docusaurus/docs/Android/01-basics/01-introduction.mdx b/docusaurus/docs/Android/01-basics/01-introduction.mdx index 724cd51c4c..bc16f315ac 100644 --- a/docusaurus/docs/Android/01-basics/01-introduction.mdx +++ b/docusaurus/docs/Android/01-basics/01-introduction.mdx @@ -7,7 +7,7 @@ slug: / Welcome to the Stream Video SDK - a comprehensive toolkit designed to help you swiftly implement features such as video calling, audio calling, audio rooms, and livestreaming within your app. Our goal is to ensure an optimal developer experience that enables your application to go live within days. -Our Kotlin SDK is furnished with user-friendly UI components and versatile StateFlow objects, making your development process seamless. +Our Compose SDK is furnished with user-friendly UI components and versatile StateFlow objects, making your development process seamless. Moreover, all calls are routed through Stream's global edge network, thereby ensuring lower latency and higher reliability due to proximity to end users. If you're new to Stream Video SDK, we recommend starting with the following three tutorials: @@ -16,7 +16,7 @@ If you're new to Stream Video SDK, we recommend starting with the following thre * ** [Audio Room Tutorial](https://getstream.io/video/sdk/android/tutorial/audio-room/) ** * ** [Livestream Tutorial](https://getstream.io/video/sdk/android/tutorial/livestreaming/) ** -After the tutorials the documentation explains how to use the +After the tutorials, the documentation explains how to use the * Core concepts such as initiating a call, switching the camera view, and more * Effective utilization of our UI components