├── .github ├── CODEOWNERS └── workflows │ └── android.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── wisemuji │ │ └── zoomclone │ │ ├── ZoomCloneComposeApplication.kt │ │ ├── model │ │ └── MeetingOptions.kt │ │ └── ui │ │ ├── AppNavHost.kt │ │ ├── MainActivity.kt │ │ ├── component │ │ ├── DefaultHorizontalDivider.kt │ │ ├── StatusBarColor.kt │ │ ├── ZoomFullSizeButton.kt │ │ ├── ZoomSwitch.kt │ │ ├── ZoomTextField.kt │ │ └── ZoomVideoTheme.kt │ │ ├── joinmeeting │ │ ├── JoinMeetingScreen.kt │ │ └── navigation │ │ │ └── JoinMeetingNavigation.kt │ │ ├── lobby │ │ ├── LobbyScreen.kt │ │ └── navigation │ │ │ └── LobbyNavigation.kt │ │ ├── meetingroom │ │ ├── MeetingRoomScreen.kt │ │ ├── MeetingRoomViewModel.kt │ │ ├── ReactionItemData.kt │ │ ├── ToggleButton.kt │ │ └── navigation │ │ │ └── MeetingRoomNavigation.kt │ │ ├── newmeeting │ │ ├── NewMeetingScreen.kt │ │ └── navigation │ │ │ └── NewMeetingNavigation.kt │ │ └── theme │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ └── res │ ├── drawable │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── ic_zoom_breakout.xml │ ├── ic_zoom_chat.xml │ ├── ic_zoom_join_meeting.xml │ ├── ic_zoom_mic_off.xml │ ├── ic_zoom_mic_on.xml │ ├── ic_zoom_participants.xml │ ├── ic_zoom_reaction.xml │ ├── ic_zoom_video_off.xml │ └── ic_zoom_video_on.xml │ ├── mipmap-anydpi │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build-logic ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ ├── com │ └── wisemuji │ │ └── zoomclone │ │ ├── ComposeAndroid.kt │ │ ├── Extension.kt │ │ ├── HiltAndroid.kt │ │ ├── KotlinAndroid.kt │ │ └── Spotless.kt │ ├── zoomclone.android.application.gradle.kts │ └── zoomclone.android.compose.gradle.kts ├── build.gradle.kts ├── figures ├── cover.jpg ├── stream0.png ├── stream1.png ├── stream2.png ├── stream3.png ├── stream4.png ├── stream5.png └── stream6.png ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── previews ├── preview0.png └── preview1.png ├── secrets.defaults.properties ├── settings.gradle.kts └── spotless ├── copyright.kt ├── copyright.kts └── spotless.gradle.kts /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # More details are here: https://help.github.com/articles/about-codeowners/ 5 | 6 | # The '*' pattern is global owners. 7 | # Not adding in this PR, but I'd like to try adding a global owner set with the entire team. 8 | # One interpretation of their docs is that global owners are added only if not removed 9 | # by a more local rule. 10 | 11 | # Order is important. The last matching pattern has the most precedence. 12 | # The folders are ordered as follows: 13 | 14 | # In each subsection folders are ordered first by depth, then alphabetically. 15 | # This should make it easy to add new rules without breaking existing ones. 16 | * @wisemuji -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: set up JDK 16 | uses: actions/setup-java@v1 17 | with: 18 | distribution: adopt 19 | java-version: 17 20 | 21 | - name: Cache Gradle and wrapper 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.gradle/caches 26 | ~/.gradle/wrapper 27 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} 28 | restore-keys: | 29 | ${{ runner.os }}-gradle- 30 | - name: Make Gradle executable 31 | run: chmod +x ./gradlew 32 | 33 | - name: Build with Gradle 34 | run: ./gradlew build 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | secrets.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # Intellij 37 | *.iml 38 | /.idea/* 39 | !.idea/codeInsightSettings.xml 40 | app/.idea/ 41 | 42 | # Mac 43 | *.DS_Store 44 | 45 | # Keystore files 46 | *.jks 47 | 48 | # External native build folder generated in Android Studio 2.2 and later 49 | .externalNativeBuild 50 | 51 | # Google Services (e.g. APIs or Firebase) 52 | google-services.json 53 | 54 | # Temporary API docs 55 | docs/api 56 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | Suhyeon. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute 2 | We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. 3 | 4 | ## Code reviews 5 | All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) for more information on using pull requests. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Blog-Build a Zoom Clone with Jetpack Compose-2000x840px](figures/cover.jpg) 2 | 3 |

4 | License 5 | API 6 | Build Status 7 | Kotlin Weekly 8 |

9 | 10 | This is a [Zoom](https://zoom.us/) clone app built with __[Stream Video SDK for Compose](https://getstream.io/video/sdk/android/?utm_source=Github&utm_medium=external_write[…]_campaign=Github_Mar2024_ZoomAndroidClone&utm_term=suhyeon)__ to implement real-time video meeting features using Jetpack Compose. 11 | 12 | The goal of this repository is to showcase the following: 13 | 14 | - The development of comprehensive UI elements utilizing Jetpack Compose. 15 | - The use of Android architecture components alongside Jetpack libraries, including androidx ViewModel and Hilt. 16 | - Execution of background operations using Kotlin Coroutines. 17 | - Integration of real-time video meeting room functionalities through the Stream Video SDK, powered by WebRTC technology. 18 | 19 | ## ✍️ Technical Content 20 | 21 | If you're interested in building this project from the scratch, check out the blog posts below: 22 | 23 | - **[Build a Real-Time Zoom Clone with Jetpack Compose](https://getstream.io/blog/zoom-clone-compose/)** 24 | 25 | ## 📲 Download APK 26 | Go to the [Releases](https://github.com/wisemuji/zoom-clone-compose/releases) to download the latest APK. 27 | 28 | 29 | 30 | 31 | 32 | ## 🛥 Stream Video SDK 33 | **Zoom Clone Compose** is built with __[Stream Video SDK for Compose](https://getstream.io/video/sdk/android/?utm_source=Github&utm_medium=external_write[…]_campaign=Github_Mar2024_ZoomAndroidClone&utm_term=suhyeon)__ to implement a real-time video meeting features. If you’re interested in adding powerful In-App Video calling, audio room, livestreaming to your app, check out the __[Android Video Calling Tutorial](https://getstream.io/video/sdk/android/tutorial/video-calling/?utm_source=Github&utm_medium=external_write[…]_campaign=Github_Mar2024_ZoomAndroidClone&utm_term=suhyeon)__! 34 | 35 | ### Stream Video 36 | 37 | - [Stream Video SDK for Android on GitHub](https://github.com/getstream/stream-video-android?utm_source=Github&utm_medium=external_write[…]_campaign=Github_Mar2024_ZoomAndroidClone&utm_term=suhyeon) 38 | - [Video Call Tutorial](https://getstream.io/video/docs/android/tutorials/video-calling?utm_source=Github&utm_medium=external_write[…]_campaign=Github_Mar2024_ZoomAndroidClone&utm_term=suhyeon) 39 | - [Audio Room Tutorial](https://getstream.io/video/docs/android/tutorials/audio-room?utm_source=Github&utm_medium=external_write[…]_campaign=Github_Mar2024_ZoomAndroidClone&utm_term=suhyeon) 40 | - [Livestream Tutorial](https://getstream.io/video/docs/android/tutorials/livestream?utm_source=Github&utm_medium=external_write[…]_campaign=Github_Mar2024_ZoomAndroidClone&utm_term=suhyeon) 41 | 42 | ## 📷 Previews 43 | 44 | ![preview0](previews/preview0.png) 45 | ![preview1](previews/preview1.png) 46 | 47 | ## 💻 Build Your Own Chat Project 48 | 49 | If you want to build your own chat project, you should follow the instructions below: 50 | 51 | 1. Go to the __[Stream login page](https://getstream.io/try-for-free?utm_source=Github&utm_medium=external_write[…]_campaign=Github_Mar2024_ZoomAndroidClone&utm_term=suhyeon)__. 52 | 2. If you have your GitHub account, click the **SIGN UP WITH GITHUB** button and you can sign up within a couple of seconds. 53 | 54 | ![stream](figures/stream0.png) 55 | 56 | 3. If you don't have a GitHub account, fill in the inputs and click the **START FREE TRIAL** button. 57 | 4. Go to the __[Dashboard](https://dashboard.getstream.io?utm_source=Github&utm_medium=external_write[…]_campaign=Github_Mar2024_ZoomAndroidClone&utm_term=suhyeon)__ and click the **Create App** button like the below. 58 | 59 | ![stream](figures/stream1.png) 60 | 61 | 5. Fill in the blanks like the below and click the **Create App** button. 62 | 63 | ![stream](figures/stream2.png) 64 | 65 | 6. You will see the **Key** like the figure below and then copy it. 66 | 67 | ![stream](figures/stream3.png) 68 | 69 | 7. Create a `secrets.properties` file on the root project directory with the text below using your API key: 70 | 71 | ``` 72 | STREAM_API_KEY=REPLACE WITH YOUR API KEY 73 | ``` 74 | 75 | 8. Build and run the project. 76 | 77 | ## 🛠 Tech Stack & Open Source Libraries 78 | - Minimum SDK level 26. 79 | - 100% [Jetpack Compose](https://developer.android.com/jetpack/compose) based + [Coroutines](https://github.com/Kotlin/kotlinx.coroutines) + [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/) for asynchronous. 80 | - [Compose Video SDK for Real-Time Meeting](https://getstream.io/video/docs/android/tutorials/video-calling?utm_source=Github&utm_medium=external_write[…]_campaign=Github_Mar2024_ZoomAndroidClone&utm_term=suhyeon): The Jetpack Compose Chat Messaging SDK is built on a low-level chat client and provides modular, customizable Compose UI components that you can easily drop into your app. 81 | - Jetpack 82 | - Compose: Android’s modern toolkit for building native UI. 83 | - ViewModel: UI related data holder and lifecycle aware. 84 | - Navigation: For navigating screens and [Hilt Navigation Compose](https://developer.android.com/jetpack/compose/libraries#hilt) for injecting dependencies. 85 | - [Hilt](https://dagger.dev/hilt/): Dependency Injection. 86 | - [Spotless gradle plugin](https://github.com/diffplug/spotless/tree/main/plugin-gradle) for formatting kotlin files. 87 | 88 | ## 🤝 Contribution 89 | 90 | Most of the features are not completed except the chat feature, so anyone can contribute and improve this project following the [Contributing Guideline](https://github.com/wisemuji/zoom-clone-compose/blob/main/CONTRIBUTING.md). 91 | 92 | # License 93 | ```xml 94 | Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 95 | 96 | Licensed under the Apache License, Version 2.0 (the "License"); 97 | you may not use this file except in compliance with the License. 98 | You may obtain a copy of the License at 99 | 100 | http://www.apache.org/licenses/LICENSE-2.0 101 | 102 | Unless required by applicable law or agreed to in writing, software 103 | distributed under the License is distributed on an "AS IS" BASIS, 104 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 105 | See the License for the specific language governing permissions and 106 | limitations under the License. 107 | ``` 108 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | plugins { 19 | id("zoomclone.android.application") 20 | id("zoomclone.android.compose") 21 | id(libs.plugins.kotlin.serialization.get().pluginId) 22 | id(libs.plugins.google.secrets.get().pluginId) 23 | } 24 | 25 | android { 26 | namespace = "com.wisemuji.zoomclone" 27 | defaultConfig { 28 | applicationId = "com.wisemuji.zoomclone" 29 | targetSdk = 34 30 | versionCode = 1 31 | versionName = "1.0" 32 | 33 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 34 | vectorDrawables { 35 | useSupportLibrary = true 36 | } 37 | } 38 | buildFeatures { 39 | buildConfig = true 40 | } 41 | packaging { 42 | resources { 43 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 44 | } 45 | } 46 | } 47 | 48 | secrets { 49 | propertiesFileName = "secrets.properties" 50 | defaultPropertiesFileName = "secrets.defaults.properties" 51 | ignoreList.add("keyToIgnore") // Ignore the key "keyToIgnore" 52 | ignoreList.add("sdk.*") // Ignore all keys matching the regexp "sdk.*" 53 | } 54 | 55 | dependencies { 56 | // stream 57 | implementation(libs.stream.video.ui.compose) 58 | implementation(libs.stream.video.ui.previewdata) 59 | 60 | // androidx 61 | implementation(libs.androidx.core.ktx) 62 | implementation(libs.androidx.activity.compose) 63 | implementation(libs.androidx.lifecycle.runtimeCompose) 64 | implementation(libs.androidx.lifecycle.viewModelCompose) 65 | implementation(libs.androidx.compose.navigation) 66 | 67 | // accompanist 68 | implementation(libs.accompanist.permissions) 69 | 70 | // kotlin 71 | implementation(libs.kotlinx.serialization.json) 72 | } 73 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ZoomCloneComposeApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone 18 | 19 | import android.app.Application 20 | import dagger.hilt.android.HiltAndroidApp 21 | import io.getstream.video.android.core.StreamVideo 22 | import io.getstream.video.android.core.StreamVideoBuilder 23 | import io.getstream.video.android.model.User 24 | 25 | @HiltAndroidApp 26 | class ZoomCloneComposeApplication : Application() { 27 | override fun onCreate() { 28 | super.onCreate() 29 | 30 | initStreamVideo() 31 | } 32 | 33 | private fun initStreamVideo() { 34 | val userId = "wisemuji" 35 | StreamVideoBuilder( 36 | context = applicationContext, 37 | apiKey = BuildConfig.STREAM_API_KEY, 38 | token = StreamVideo.devToken(userId), 39 | user = User( 40 | id = userId, 41 | name = "Wisemuji", 42 | image = "http://placekitten.com/200/300", 43 | role = "admin", 44 | ), 45 | ).build() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/model/MeetingOptions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.model 18 | 19 | import kotlinx.serialization.Serializable 20 | 21 | @Serializable 22 | data class MeetingOptions( 23 | val meetingId: String = "", 24 | val username: String = "", 25 | val audioOn: Boolean = true, 26 | val videoOn: Boolean = true, 27 | ) 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/AppNavHost.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Modifier 21 | import androidx.navigation.NavHostController 22 | import androidx.navigation.compose.NavHost 23 | import com.wisemuji.zoomclone.ui.joinmeeting.navigation.joinMeetingScreen 24 | import com.wisemuji.zoomclone.ui.joinmeeting.navigation.navigateToJoinMeeting 25 | import com.wisemuji.zoomclone.ui.lobby.navigation.LobbyRoute 26 | import com.wisemuji.zoomclone.ui.lobby.navigation.lobbyScreen 27 | import com.wisemuji.zoomclone.ui.meetingroom.navigation.meetingRoomScreen 28 | import com.wisemuji.zoomclone.ui.meetingroom.navigation.navigateToMeetingRoom 29 | import com.wisemuji.zoomclone.ui.newmeeting.navigation.navigateToNewMeeting 30 | import com.wisemuji.zoomclone.ui.newmeeting.navigation.newMeetingScreen 31 | 32 | @Composable 33 | fun AppNavHost( 34 | navController: NavHostController, 35 | modifier: Modifier = Modifier, 36 | startDestination: String = LobbyRoute.ROUTE, 37 | ) { 38 | NavHost( 39 | modifier = modifier, 40 | navController = navController, 41 | startDestination = startDestination, 42 | ) { 43 | lobbyScreen( 44 | onNewMeetingClick = navController::navigateToNewMeeting, 45 | onJoinMeetingClick = navController::navigateToJoinMeeting, 46 | ) 47 | newMeetingScreen( 48 | onBackPressed = navController::popBackStack, 49 | onJoinMeetingClick = navController::navigateToMeetingRoom, 50 | ) 51 | joinMeetingScreen( 52 | onBackPressed = navController::popBackStack, 53 | onJoinMeetingClick = navController::navigateToMeetingRoom, 54 | ) 55 | meetingRoomScreen(onBackPressed = navController::popBackStack) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui 18 | 19 | import android.os.Bundle 20 | import androidx.activity.ComponentActivity 21 | import androidx.activity.compose.setContent 22 | import androidx.compose.foundation.layout.fillMaxSize 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.Surface 25 | import androidx.compose.ui.Modifier 26 | import androidx.navigation.compose.rememberNavController 27 | import com.wisemuji.zoomclone.ui.theme.ZoomCloneComposeTheme 28 | import dagger.hilt.android.AndroidEntryPoint 29 | 30 | @AndroidEntryPoint 31 | class MainActivity : ComponentActivity() { 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | setContent { 35 | ZoomCloneComposeTheme { 36 | Surface( 37 | modifier = Modifier.fillMaxSize(), 38 | color = MaterialTheme.colorScheme.background, 39 | ) { 40 | AppNavHost(navController = rememberNavController()) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/component/DefaultHorizontalDivider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.component 18 | 19 | import androidx.compose.material3.HorizontalDivider 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.unit.dp 24 | 25 | @Composable 26 | fun DefaultHorizontalDivider( 27 | modifier: Modifier = Modifier, 28 | ) { 29 | HorizontalDivider( 30 | color = MaterialTheme.colorScheme.outlineVariant, 31 | thickness = 1.dp, 32 | modifier = modifier, 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/component/StatusBarColor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.component 18 | 19 | import android.app.Activity 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.SideEffect 22 | import androidx.compose.ui.graphics.Color 23 | import androidx.compose.ui.graphics.toArgb 24 | import androidx.compose.ui.platform.LocalView 25 | import androidx.core.view.WindowCompat 26 | 27 | @Composable 28 | fun StatusBarColor( 29 | color: Color, 30 | isIconLight: Boolean = false, 31 | ) { 32 | val view = LocalView.current 33 | if (!view.isInEditMode) { 34 | SideEffect { 35 | val window = (view.context as Activity).window 36 | window.statusBarColor = color.toArgb() 37 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = isIconLight 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/component/ZoomFullSizeButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.component 18 | 19 | import androidx.compose.foundation.layout.fillMaxWidth 20 | import androidx.compose.foundation.layout.padding 21 | import androidx.compose.foundation.shape.RoundedCornerShape 22 | import androidx.compose.material3.Button 23 | import androidx.compose.material3.Text 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.text.font.FontWeight 27 | import androidx.compose.ui.tooling.preview.Preview 28 | import androidx.compose.ui.unit.dp 29 | import androidx.compose.ui.unit.sp 30 | import com.wisemuji.zoomclone.ui.theme.ZoomCloneComposeTheme 31 | 32 | @Composable 33 | fun ZoomFullSizeButton( 34 | text: String, 35 | onClick: () -> Unit, 36 | modifier: Modifier = Modifier, 37 | enabled: Boolean = true, 38 | ) { 39 | Button( 40 | onClick = onClick, 41 | enabled = enabled, 42 | shape = RoundedCornerShape(14.dp), 43 | modifier = modifier 44 | .fillMaxWidth(), 45 | ) { 46 | Text( 47 | text = text, 48 | fontSize = 17.sp, 49 | fontWeight = FontWeight.Bold, 50 | modifier = Modifier.padding(vertical = 8.dp), 51 | ) 52 | } 53 | } 54 | 55 | @Preview 56 | @Composable 57 | private fun FullSizeButtonPreview() { 58 | ZoomCloneComposeTheme { 59 | ZoomFullSizeButton(text = "Start a Meeting", onClick = {}) 60 | } 61 | } 62 | 63 | @Preview(showBackground = true) 64 | @Composable 65 | private fun FullSizeButtonPreviewDisabled() { 66 | ZoomCloneComposeTheme { 67 | ZoomFullSizeButton(text = "Start a Meeting", onClick = {}, enabled = false) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/component/ZoomSwitch.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.component 18 | 19 | import androidx.compose.foundation.background 20 | import androidx.compose.foundation.clickable 21 | import androidx.compose.foundation.layout.Column 22 | import androidx.compose.foundation.layout.Row 23 | import androidx.compose.foundation.layout.padding 24 | import androidx.compose.material3.MaterialTheme 25 | import androidx.compose.material3.Switch 26 | import androidx.compose.material3.SwitchDefaults 27 | import androidx.compose.material3.Text 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.tooling.preview.Preview 32 | import androidx.compose.ui.tooling.preview.PreviewParameter 33 | import androidx.compose.ui.tooling.preview.PreviewParameterProvider 34 | import androidx.compose.ui.unit.dp 35 | import androidx.compose.ui.unit.sp 36 | import com.wisemuji.zoomclone.ui.theme.Gray60 37 | import com.wisemuji.zoomclone.ui.theme.ZoomCloneComposeTheme 38 | 39 | @Composable 40 | fun ZoomSwitch( 41 | checked: Boolean, 42 | onCheckedChange: (Boolean) -> Unit, 43 | modifier: Modifier = Modifier, 44 | ) { 45 | Switch( 46 | checked = checked, 47 | onCheckedChange = onCheckedChange, 48 | colors = SwitchDefaults.colors( 49 | checkedThumbColor = MaterialTheme.colorScheme.onTertiary, 50 | checkedTrackColor = MaterialTheme.colorScheme.tertiary, 51 | uncheckedThumbColor = MaterialTheme.colorScheme.outlineVariant, 52 | uncheckedBorderColor = MaterialTheme.colorScheme.outlineVariant, 53 | ), 54 | modifier = modifier, 55 | ) 56 | } 57 | 58 | @Composable 59 | fun ZoomSwitchRow( 60 | title: String, 61 | checked: Boolean, 62 | onCheckedChange: (Boolean) -> Unit, 63 | modifier: Modifier = Modifier, 64 | subtitle: String? = null, 65 | ) { 66 | Row( 67 | verticalAlignment = Alignment.CenterVertically, 68 | modifier = modifier 69 | .clickable { onCheckedChange(!checked) } 70 | .background(MaterialTheme.colorScheme.surface) 71 | .padding(horizontal = 14.dp), 72 | ) { 73 | Column( 74 | modifier = Modifier 75 | .weight(1f) 76 | .padding(vertical = 10.dp), 77 | ) { 78 | Text( 79 | text = title, 80 | fontSize = 15.sp, 81 | color = MaterialTheme.colorScheme.onSurface, 82 | ) 83 | subtitle?.let { 84 | Text( 85 | text = it, 86 | fontSize = 13.sp, 87 | color = Gray60, 88 | ) 89 | } 90 | } 91 | ZoomSwitch(checked, onCheckedChange = onCheckedChange) 92 | } 93 | } 94 | 95 | class CheckedPreviewParameterProvider : PreviewParameterProvider { 96 | override val values = sequenceOf(true, false) 97 | } 98 | 99 | @Preview 100 | @Composable 101 | private fun ZoomSwitchPreview( 102 | @PreviewParameter(CheckedPreviewParameterProvider::class) checked: Boolean, 103 | ) { 104 | ZoomCloneComposeTheme { 105 | ZoomSwitch(checked = checked, onCheckedChange = {}) 106 | } 107 | } 108 | 109 | @Preview 110 | @Composable 111 | private fun ZoomSwitchRowPreview( 112 | @PreviewParameter(CheckedPreviewParameterProvider::class) checked: Boolean, 113 | ) { 114 | ZoomCloneComposeTheme { 115 | ZoomSwitchRow( 116 | title = "Video On", 117 | checked = checked, 118 | onCheckedChange = {}, 119 | ) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/component/ZoomTextField.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.component 18 | 19 | import androidx.compose.foundation.layout.fillMaxWidth 20 | import androidx.compose.material3.LocalTextStyle 21 | import androidx.compose.material3.Text 22 | import androidx.compose.material3.TextField 23 | import androidx.compose.material3.TextFieldDefaults 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.text.style.TextAlign 27 | import androidx.compose.ui.tooling.preview.Preview 28 | import com.wisemuji.zoomclone.ui.theme.Gray20 29 | import com.wisemuji.zoomclone.ui.theme.Gray50 30 | import com.wisemuji.zoomclone.ui.theme.ZoomCloneComposeTheme 31 | 32 | @Composable 33 | fun ZoomTextField( 34 | value: String, 35 | onValueChange: (String) -> Unit, 36 | placeholderText: String, 37 | modifier: Modifier = Modifier, 38 | ) { 39 | TextField( 40 | value = value, 41 | onValueChange = onValueChange, 42 | placeholder = { 43 | Text( 44 | text = placeholderText, 45 | textAlign = TextAlign.Center, 46 | modifier = Modifier.fillMaxWidth(), 47 | ) 48 | }, 49 | textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), 50 | colors = TextFieldDefaults.colors( 51 | focusedPlaceholderColor = Gray50, 52 | unfocusedPlaceholderColor = Gray50, 53 | focusedIndicatorColor = Gray20, 54 | unfocusedIndicatorColor = Gray20, 55 | ), 56 | modifier = modifier.fillMaxWidth(), 57 | ) 58 | } 59 | 60 | @Preview 61 | @Composable 62 | fun ZoomTextFieldPreview() { 63 | ZoomCloneComposeTheme { 64 | ZoomTextField( 65 | value = "", 66 | onValueChange = {}, 67 | placeholderText = "Meeting ID", 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/component/ZoomVideoTheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.component 18 | 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.graphics.RectangleShape 22 | import io.getstream.video.android.compose.theme.StreamColors 23 | import io.getstream.video.android.compose.theme.StreamShapes 24 | import io.getstream.video.android.compose.theme.VideoTheme 25 | 26 | @Composable 27 | fun ZoomVideoTheme( 28 | content: @Composable () -> Unit, 29 | ) { 30 | VideoTheme( 31 | colors = StreamColors 32 | .defaultColors() 33 | .copy( 34 | appBackground = MaterialTheme.colorScheme.inverseSurface, 35 | barsBackground = MaterialTheme.colorScheme.inverseSurface, 36 | inputBackground = MaterialTheme.colorScheme.inverseSurface, 37 | textHighEmphasis = MaterialTheme.colorScheme.inverseOnSurface, 38 | ), 39 | shapes = StreamShapes 40 | .defaultShapes() 41 | .copy( 42 | participantContainerShape = RectangleShape, 43 | callControls = RectangleShape, 44 | ), 45 | content = content, 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/joinmeeting/JoinMeetingScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.joinmeeting 18 | 19 | import androidx.compose.foundation.background 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.foundation.layout.Column 22 | import androidx.compose.foundation.layout.Spacer 23 | import androidx.compose.foundation.layout.fillMaxSize 24 | import androidx.compose.foundation.layout.fillMaxWidth 25 | import androidx.compose.foundation.layout.height 26 | import androidx.compose.foundation.layout.padding 27 | import androidx.compose.foundation.layout.size 28 | import androidx.compose.material.icons.Icons 29 | import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft 30 | import androidx.compose.material3.Icon 31 | import androidx.compose.material3.IconButton 32 | import androidx.compose.material3.MaterialTheme 33 | import androidx.compose.material3.Scaffold 34 | import androidx.compose.material3.Text 35 | import androidx.compose.runtime.Composable 36 | import androidx.compose.runtime.getValue 37 | import androidx.compose.runtime.mutableStateOf 38 | import androidx.compose.runtime.remember 39 | import androidx.compose.runtime.setValue 40 | import androidx.compose.ui.Alignment 41 | import androidx.compose.ui.Modifier 42 | import androidx.compose.ui.res.stringResource 43 | import androidx.compose.ui.text.font.FontWeight 44 | import androidx.compose.ui.tooling.preview.Preview 45 | import androidx.compose.ui.unit.dp 46 | import androidx.compose.ui.unit.sp 47 | import com.wisemuji.zoomclone.R 48 | import com.wisemuji.zoomclone.model.MeetingOptions 49 | import com.wisemuji.zoomclone.ui.component.DefaultHorizontalDivider 50 | import com.wisemuji.zoomclone.ui.component.StatusBarColor 51 | import com.wisemuji.zoomclone.ui.component.ZoomFullSizeButton 52 | import com.wisemuji.zoomclone.ui.component.ZoomSwitchRow 53 | import com.wisemuji.zoomclone.ui.component.ZoomTextField 54 | import com.wisemuji.zoomclone.ui.theme.Gray60 55 | import com.wisemuji.zoomclone.ui.theme.ZoomCloneComposeTheme 56 | 57 | @Composable 58 | fun JoinMeetingScreen( 59 | onBackPressed: () -> Unit, 60 | onJoinMeetingClick: (MeetingOptions) -> Unit, 61 | ) { 62 | var meetingId by remember { mutableStateOf("") } 63 | var name by remember { mutableStateOf("") } 64 | var audioOn by remember { mutableStateOf(true) } 65 | var videoOn by remember { mutableStateOf(true) } 66 | 67 | StatusBarColor(color = MaterialTheme.colorScheme.surface, isIconLight = true) 68 | Scaffold( 69 | topBar = { JoinMeetingTopAppBar(onBack = onBackPressed) }, 70 | ) { innerPadding -> 71 | Column( 72 | modifier = Modifier 73 | .padding(innerPadding) 74 | .fillMaxSize() 75 | .background(MaterialTheme.colorScheme.surfaceContainer), 76 | ) { 77 | DefaultHorizontalDivider() 78 | Spacer(modifier = Modifier.padding(12.dp)) 79 | DefaultHorizontalDivider() 80 | ZoomTextField( 81 | value = meetingId, 82 | onValueChange = { meetingId = it }, 83 | placeholderText = stringResource(R.string.meeting_id_placeholder), 84 | ) 85 | ZoomTextField( 86 | value = name, 87 | onValueChange = { name = it }, 88 | placeholderText = stringResource(R.string.your_name_placeholder), 89 | ) 90 | ZoomFullSizeButton( 91 | text = stringResource(R.string.join), 92 | onClick = { onJoinMeetingClick(MeetingOptions(meetingId, name, audioOn, videoOn)) }, 93 | modifier = Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp), 94 | ) 95 | Text( 96 | text = stringResource(R.string.join_notes), 97 | fontSize = 12.sp, 98 | color = Gray60, 99 | lineHeight = 14.sp, 100 | modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 10.dp, bottom = 42.dp), 101 | ) 102 | JoinOptions( 103 | doNotConnectAudio = !audioOn, 104 | onCheckedDoNotConnectAudio = { audioOn = !it }, 105 | turnOffVideo = !videoOn, 106 | onCheckedTurnOffVideo = { videoOn = !it }, 107 | ) 108 | } 109 | } 110 | } 111 | 112 | @Composable 113 | private fun JoinMeetingTopAppBar(onBack: () -> Unit) { 114 | Box( 115 | contentAlignment = Alignment.CenterStart, 116 | modifier = Modifier 117 | .fillMaxWidth() 118 | .height(56.dp) 119 | .padding(horizontal = 4.dp), 120 | ) { 121 | IconButton(onClick = onBack) { 122 | Icon( 123 | imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft, 124 | contentDescription = "Back", 125 | tint = MaterialTheme.colorScheme.primary, 126 | modifier = Modifier.size(52.dp), 127 | ) 128 | } 129 | Text( 130 | text = stringResource(R.string.join_meeting_title), 131 | fontSize = 17.sp, 132 | fontWeight = FontWeight.Medium, 133 | modifier = Modifier.align(Alignment.Center), 134 | ) 135 | } 136 | } 137 | 138 | @Composable 139 | private fun JoinOptions( 140 | doNotConnectAudio: Boolean = false, 141 | onCheckedDoNotConnectAudio: (Boolean) -> Unit = {}, 142 | turnOffVideo: Boolean = false, 143 | onCheckedTurnOffVideo: (Boolean) -> Unit = {}, 144 | ) { 145 | Column { 146 | Text( 147 | text = stringResource(R.string.join_options), 148 | fontSize = 12.sp, 149 | color = Gray60, 150 | fontWeight = FontWeight.Medium, 151 | modifier = Modifier.padding(start = 16.dp), 152 | ) 153 | DefaultHorizontalDivider() 154 | ZoomSwitchRow( 155 | title = stringResource(R.string.do_not_connect_audio), 156 | checked = doNotConnectAudio, 157 | onCheckedChange = onCheckedDoNotConnectAudio, 158 | ) 159 | DefaultHorizontalDivider() 160 | ZoomSwitchRow( 161 | title = stringResource(R.string.turn_off_video), 162 | checked = turnOffVideo, 163 | onCheckedChange = onCheckedTurnOffVideo, 164 | ) 165 | DefaultHorizontalDivider() 166 | } 167 | } 168 | 169 | @Preview 170 | @Composable 171 | private fun JoinMeetingScreenPreview() { 172 | ZoomCloneComposeTheme { 173 | JoinMeetingScreen({}, {}) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/joinmeeting/navigation/JoinMeetingNavigation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.joinmeeting.navigation 18 | 19 | import androidx.navigation.NavController 20 | import androidx.navigation.NavGraphBuilder 21 | import androidx.navigation.compose.composable 22 | import com.wisemuji.zoomclone.model.MeetingOptions 23 | import com.wisemuji.zoomclone.ui.joinmeeting.JoinMeetingScreen 24 | import com.wisemuji.zoomclone.ui.joinmeeting.navigation.JoinMeetingRoute.ROUTE 25 | 26 | fun NavController.navigateToJoinMeeting() { 27 | navigate(ROUTE) 28 | } 29 | 30 | fun NavGraphBuilder.joinMeetingScreen( 31 | onBackPressed: () -> Unit, 32 | onJoinMeetingClick: (MeetingOptions) -> Unit, 33 | ) { 34 | composable(route = ROUTE) { 35 | JoinMeetingScreen(onBackPressed, onJoinMeetingClick) 36 | } 37 | } 38 | 39 | object JoinMeetingRoute { 40 | const val ROUTE = "join_meeting" 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/lobby/LobbyScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.lobby 18 | 19 | import androidx.compose.foundation.background 20 | import androidx.compose.foundation.clickable 21 | import androidx.compose.foundation.layout.Arrangement 22 | import androidx.compose.foundation.layout.Box 23 | import androidx.compose.foundation.layout.Column 24 | import androidx.compose.foundation.layout.Row 25 | import androidx.compose.foundation.layout.fillMaxWidth 26 | import androidx.compose.foundation.layout.height 27 | import androidx.compose.foundation.layout.padding 28 | import androidx.compose.foundation.layout.size 29 | import androidx.compose.foundation.shape.RoundedCornerShape 30 | import androidx.compose.material.icons.Icons 31 | import androidx.compose.material.icons.outlined.Info 32 | import androidx.compose.material3.AlertDialog 33 | import androidx.compose.material3.Icon 34 | import androidx.compose.material3.IconButton 35 | import androidx.compose.material3.MaterialTheme 36 | import androidx.compose.material3.Scaffold 37 | import androidx.compose.material3.Text 38 | import androidx.compose.material3.TextButton 39 | import androidx.compose.runtime.Composable 40 | import androidx.compose.runtime.getValue 41 | import androidx.compose.runtime.mutableStateOf 42 | import androidx.compose.runtime.remember 43 | import androidx.compose.runtime.setValue 44 | import androidx.compose.ui.Alignment 45 | import androidx.compose.ui.Modifier 46 | import androidx.compose.ui.draw.clip 47 | import androidx.compose.ui.graphics.Color 48 | import androidx.compose.ui.graphics.painter.Painter 49 | import androidx.compose.ui.res.painterResource 50 | import androidx.compose.ui.res.stringResource 51 | import androidx.compose.ui.text.font.FontWeight 52 | import androidx.compose.ui.tooling.preview.Preview 53 | import androidx.compose.ui.unit.dp 54 | import androidx.compose.ui.unit.sp 55 | import com.wisemuji.zoomclone.R 56 | import com.wisemuji.zoomclone.ui.component.DefaultHorizontalDivider 57 | import com.wisemuji.zoomclone.ui.component.StatusBarColor 58 | import com.wisemuji.zoomclone.ui.theme.Gray50 59 | import com.wisemuji.zoomclone.ui.theme.ZoomCloneComposeTheme 60 | 61 | @Composable 62 | fun LobbyScreen( 63 | onNewMeetingClick: () -> Unit, 64 | onJoinMeetingClick: () -> Unit, 65 | ) { 66 | var showInfoDialog by remember { mutableStateOf(false) } 67 | 68 | if (showInfoDialog) { 69 | LobbyInfoDialog(onDismissRequest = { showInfoDialog = false }) 70 | } 71 | StatusBarColor(color = MaterialTheme.colorScheme.surfaceContainerHighest, isIconLight = false) 72 | Scaffold( 73 | topBar = { LobbyScreenTopAppBar { showInfoDialog = true } }, 74 | ) { innerPadding -> 75 | Column { 76 | Row( 77 | horizontalArrangement = Arrangement.SpaceAround, 78 | modifier = Modifier 79 | .padding(innerPadding) 80 | .background(MaterialTheme.colorScheme.surfaceContainer) 81 | .padding(16.dp) 82 | .fillMaxWidth(), 83 | ) { 84 | LobbyItem( 85 | icon = painterResource(id = R.drawable.ic_zoom_video_on), 86 | caption = stringResource(R.string.new_meeting_navigator), 87 | color = MaterialTheme.colorScheme.secondary, 88 | ) { onNewMeetingClick() } 89 | LobbyItem( 90 | icon = painterResource(id = R.drawable.ic_zoom_join_meeting), 91 | caption = stringResource(R.string.join_meeting_navigator), 92 | color = MaterialTheme.colorScheme.primary, 93 | ) { onJoinMeetingClick() } 94 | LobbyItem( 95 | icon = painterResource(id = R.drawable.ic_zoom_breakout), 96 | caption = stringResource(R.string.working_in_progress), 97 | color = Gray50, 98 | ) 99 | LobbyItem( 100 | icon = painterResource(id = R.drawable.ic_zoom_breakout), 101 | caption = stringResource(R.string.working_in_progress), 102 | color = Gray50, 103 | ) 104 | } 105 | DefaultHorizontalDivider() 106 | } 107 | } 108 | } 109 | 110 | @Composable 111 | private fun LobbyScreenTopAppBar( 112 | onInfoClick: () -> Unit, 113 | ) { 114 | Box( 115 | contentAlignment = Alignment.CenterEnd, 116 | modifier = Modifier 117 | .background(MaterialTheme.colorScheme.surfaceContainerHighest) 118 | .fillMaxWidth() 119 | .height(56.dp) 120 | .padding(horizontal = 4.dp), 121 | ) { 122 | Text( 123 | text = stringResource(R.string.lobby_title), 124 | fontSize = 17.sp, 125 | fontWeight = FontWeight.Medium, 126 | modifier = Modifier.align(Alignment.Center), 127 | color = MaterialTheme.colorScheme.inverseOnSurface, 128 | ) 129 | IconButton(onClick = onInfoClick) { 130 | Icon( 131 | imageVector = Icons.Outlined.Info, 132 | contentDescription = "information", 133 | tint = MaterialTheme.colorScheme.inverseOnSurface, 134 | ) 135 | } 136 | } 137 | } 138 | 139 | @Composable 140 | private fun LobbyItem( 141 | icon: Painter, 142 | caption: String, 143 | color: Color, 144 | modifier: Modifier = Modifier, 145 | onClick: () -> Unit = {}, 146 | ) { 147 | Column( 148 | horizontalAlignment = Alignment.CenterHorizontally, 149 | verticalArrangement = Arrangement.spacedBy(4.dp), 150 | modifier = modifier, 151 | ) { 152 | Box( 153 | contentAlignment = Alignment.Center, 154 | modifier = Modifier 155 | .clip(RoundedCornerShape(16.dp)) 156 | .background(color) 157 | .size(64.dp) 158 | .clickable { onClick() }, 159 | ) { 160 | Icon( 161 | painter = icon, 162 | contentDescription = caption, 163 | tint = MaterialTheme.colorScheme.onPrimary, 164 | ) 165 | } 166 | Text( 167 | text = caption, 168 | fontSize = 12.sp, 169 | color = MaterialTheme.colorScheme.onSurfaceVariant, 170 | letterSpacing = (-0.45).sp, 171 | ) 172 | } 173 | } 174 | 175 | @Composable 176 | private fun LobbyInfoDialog( 177 | onDismissRequest: () -> Unit, 178 | ) { 179 | AlertDialog( 180 | title = { 181 | Text(text = stringResource(id = R.string.app_information_title)) 182 | }, 183 | text = { 184 | Text(text = stringResource(id = R.string.app_information_content)) 185 | }, 186 | onDismissRequest = { 187 | onDismissRequest() 188 | }, 189 | confirmButton = { 190 | TextButton(onClick = { onDismissRequest() }) { 191 | Text(text = stringResource(id = R.string.dialog_confirm)) 192 | } 193 | }, 194 | ) 195 | } 196 | 197 | @Preview 198 | @Composable 199 | private fun LobbyScreenPreview() { 200 | ZoomCloneComposeTheme { 201 | LobbyScreen({}, {}) 202 | } 203 | } 204 | 205 | @Preview(showBackground = true) 206 | @Composable 207 | private fun LobbyScreenTopAppBarPreview() { 208 | ZoomCloneComposeTheme { 209 | LobbyScreenTopAppBar {} 210 | } 211 | } 212 | 213 | @Preview(showBackground = true) 214 | @Composable 215 | private fun LobbyItemPreview() { 216 | ZoomCloneComposeTheme { 217 | LobbyItem( 218 | icon = painterResource(id = R.drawable.ic_zoom_video_on), 219 | caption = "New Meeting", 220 | color = MaterialTheme.colorScheme.secondary, 221 | ) {} 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/lobby/navigation/LobbyNavigation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.lobby.navigation 18 | 19 | import androidx.navigation.NavGraphBuilder 20 | import androidx.navigation.compose.composable 21 | import com.wisemuji.zoomclone.ui.lobby.LobbyScreen 22 | import com.wisemuji.zoomclone.ui.lobby.navigation.LobbyRoute.ROUTE 23 | 24 | fun NavGraphBuilder.lobbyScreen( 25 | onNewMeetingClick: () -> Unit, 26 | onJoinMeetingClick: () -> Unit, 27 | ) { 28 | composable(route = ROUTE) { 29 | LobbyScreen( 30 | onNewMeetingClick = onNewMeetingClick, 31 | onJoinMeetingClick = onJoinMeetingClick, 32 | ) 33 | } 34 | } 35 | 36 | object LobbyRoute { 37 | const val ROUTE = "lobby" 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/meetingroom/MeetingRoomScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.meetingroom 18 | 19 | import android.Manifest 20 | import android.os.Build 21 | import androidx.compose.foundation.background 22 | import androidx.compose.foundation.layout.fillMaxSize 23 | import androidx.compose.foundation.layout.fillMaxWidth 24 | import androidx.compose.foundation.layout.navigationBarsPadding 25 | import androidx.compose.foundation.layout.padding 26 | import androidx.compose.foundation.layout.size 27 | import androidx.compose.material.icons.Icons 28 | import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft 29 | import androidx.compose.material3.Icon 30 | import androidx.compose.material3.IconButton 31 | import androidx.compose.material3.MaterialTheme 32 | import androidx.compose.material3.Scaffold 33 | import androidx.compose.material3.Text 34 | import androidx.compose.runtime.Composable 35 | import androidx.compose.runtime.DisposableEffect 36 | import androidx.compose.runtime.LaunchedEffect 37 | import androidx.compose.runtime.getValue 38 | import androidx.compose.runtime.mutableStateOf 39 | import androidx.compose.runtime.remember 40 | import androidx.compose.runtime.setValue 41 | import androidx.compose.ui.Alignment 42 | import androidx.compose.ui.Modifier 43 | import androidx.compose.ui.platform.LocalContext 44 | import androidx.compose.ui.res.stringResource 45 | import androidx.compose.ui.text.style.TextAlign 46 | import androidx.compose.ui.tooling.preview.Preview 47 | import androidx.compose.ui.unit.dp 48 | import androidx.hilt.navigation.compose.hiltViewModel 49 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 50 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 51 | import com.google.accompanist.permissions.rememberMultiplePermissionsState 52 | import com.wisemuji.zoomclone.R 53 | import com.wisemuji.zoomclone.ui.component.StatusBarColor 54 | import com.wisemuji.zoomclone.ui.component.ZoomVideoTheme 55 | import io.getstream.video.android.compose.ui.components.call.CallAppBar 56 | import io.getstream.video.android.compose.ui.components.call.controls.ControlActions 57 | import io.getstream.video.android.compose.ui.components.call.controls.actions.DefaultOnCallActionHandler 58 | import io.getstream.video.android.compose.ui.components.call.controls.actions.FlipCameraAction 59 | import io.getstream.video.android.compose.ui.components.call.controls.actions.LeaveCallAction 60 | import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantVideo 61 | import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantsLayout 62 | import io.getstream.video.android.compose.ui.components.call.renderer.RegularVideoRendererStyle 63 | import io.getstream.video.android.core.Call 64 | import io.getstream.video.android.core.call.state.LeaveCall 65 | import io.getstream.video.android.core.mapper.ReactionMapper 66 | import io.getstream.video.android.mock.StreamPreviewDataUtils 67 | import io.getstream.video.android.mock.previewCall 68 | 69 | @Composable 70 | fun MeetingRoomScreen( 71 | onBackPressed: () -> Unit, 72 | viewModel: MeetingRoomViewModel = hiltViewModel(), 73 | ) { 74 | val uiState by viewModel.uiState.collectAsStateWithLifecycle() 75 | 76 | StatusBarColor(color = MaterialTheme.colorScheme.inverseSurface, isIconLight = false) 77 | EnsureVideoCallPermissions { 78 | viewModel.loadMeeting() 79 | } 80 | when (uiState) { 81 | is MeetingUiState.Success -> { 82 | val call = (uiState as MeetingUiState.Success).call 83 | MeetingRoomContent(call = call, onBackPressed) 84 | } 85 | 86 | MeetingUiState.Loading -> { 87 | MeetingRoomPlaceholder(text = stringResource(R.string.connecting)) 88 | } 89 | 90 | MeetingUiState.Error -> { 91 | MeetingRoomPlaceholder(text = stringResource(R.string.error)) 92 | } 93 | } 94 | } 95 | 96 | @Composable 97 | fun MeetingRoomContent( 98 | call: Call, 99 | onLeaveCall: () -> Unit, 100 | ) { 101 | var isShowingReactionDialog by remember { mutableStateOf(false) } 102 | DisposableEffect(key1 = call.id) { 103 | onDispose { call.leave() } 104 | } 105 | ZoomVideoTheme { 106 | Scaffold( 107 | topBar = { 108 | MeetingRoomTopAppBar( 109 | call = call, 110 | onLeaveCall = onLeaveCall, 111 | ) 112 | }, 113 | bottomBar = { 114 | MeetingRoomBottomBar( 115 | call = call, 116 | onLeaveCall = onLeaveCall, 117 | toggleReactions = { isShowingReactionDialog = true }, 118 | ) 119 | }, 120 | ) { 121 | ParticipantsLayout( 122 | call = call, 123 | modifier = Modifier 124 | .padding(it) 125 | .fillMaxSize(), 126 | style = RegularVideoRendererStyle(reactionPosition = Alignment.Center), 127 | videoRenderer = { videoModifier, videoCall, videoParticipant, videoStyle -> 128 | ParticipantVideo( 129 | modifier = videoModifier, 130 | call = videoCall, 131 | participant = videoParticipant, 132 | style = videoStyle, 133 | connectionIndicatorContent = {}, 134 | ) 135 | }, 136 | ) 137 | if (isShowingReactionDialog) { 138 | ReactionsMenu( 139 | call = call, 140 | reactionMapper = ReactionMapper.defaultReactionMapper(), 141 | onDismiss = { isShowingReactionDialog = false }, 142 | ) 143 | } 144 | } 145 | } 146 | } 147 | 148 | @Composable 149 | private fun MeetingRoomTopAppBar( 150 | call: Call, 151 | onLeaveCall: () -> Unit, 152 | ) { 153 | CallAppBar( 154 | call = call, 155 | leadingContent = { 156 | IconButton(onClick = { onLeaveCall() }) { 157 | Icon( 158 | imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft, 159 | contentDescription = "Back", 160 | tint = MaterialTheme.colorScheme.inverseOnSurface, 161 | modifier = Modifier.size(52.dp), 162 | ) 163 | } 164 | FlipCameraAction( 165 | onCallAction = { call.camera.flip() }, 166 | ) 167 | }, 168 | title = stringResource(R.string.meeting_room_title), 169 | trailingContent = { 170 | LeaveCallAction( 171 | modifier = Modifier.size(52.dp), 172 | onCallAction = { onLeaveCall() }, 173 | ) 174 | }, 175 | modifier = Modifier 176 | .background(MaterialTheme.colorScheme.inverseSurface), 177 | ) 178 | } 179 | 180 | @Composable 181 | private fun MeetingRoomBottomBar( 182 | call: Call, 183 | onLeaveCall: () -> Unit, 184 | toggleReactions: () -> Unit, 185 | ) { 186 | ControlActions( 187 | modifier = Modifier.navigationBarsPadding(), 188 | call = call, 189 | onCallAction = { action -> 190 | when (action) { 191 | is LeaveCall -> { 192 | onLeaveCall() 193 | } 194 | 195 | else -> DefaultOnCallActionHandler.onCallAction(call, action) 196 | } 197 | }, 198 | actions = listOf( 199 | { ToggleMicrophoneButton(call) }, 200 | { ToggleCameraButton(call) }, 201 | { ToggleReactionsButton(toggleReactions) }, 202 | ), 203 | ) 204 | } 205 | 206 | @Composable 207 | private fun MeetingRoomPlaceholder( 208 | text: String, 209 | ) { 210 | Scaffold( 211 | containerColor = MaterialTheme.colorScheme.inverseSurface, 212 | ) { innerPadding -> 213 | Text( 214 | text = text, 215 | textAlign = TextAlign.Center, 216 | color = MaterialTheme.colorScheme.inverseOnSurface, 217 | modifier = Modifier 218 | .padding(innerPadding) 219 | .padding(16.dp) 220 | .fillMaxWidth(), 221 | ) 222 | } 223 | } 224 | 225 | @OptIn(ExperimentalPermissionsApi::class) 226 | @Composable 227 | private fun EnsureVideoCallPermissions(onPermissionsGranted: () -> Unit) { 228 | // While the SDK will handle the microphone permission, 229 | // its not a bad idea to do it prior to entering any call UIs 230 | val permissionsState = rememberMultiplePermissionsState( 231 | permissions = buildList { 232 | // Access to camera & microphone 233 | add(Manifest.permission.CAMERA) 234 | add(Manifest.permission.RECORD_AUDIO) 235 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 236 | // Allow for foreground service for notification on API 26+ 237 | add(Manifest.permission.FOREGROUND_SERVICE) 238 | } 239 | }, 240 | ) 241 | 242 | LaunchedEffect(key1 = Unit) { 243 | permissionsState.launchMultiplePermissionRequest() 244 | } 245 | 246 | LaunchedEffect(key1 = permissionsState.allPermissionsGranted) { 247 | if (permissionsState.allPermissionsGranted) { 248 | onPermissionsGranted() 249 | } 250 | } 251 | } 252 | 253 | @Preview 254 | @Composable 255 | private fun MeetingRoomContentPreview() { 256 | StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) 257 | MeetingRoomContent(previewCall) {} 258 | } 259 | 260 | @Preview 261 | @Composable 262 | private fun MeetingRoomError() { 263 | MeetingRoomPlaceholder(text = stringResource(R.string.error)) 264 | } 265 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/meetingroom/MeetingRoomViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.meetingroom 18 | 19 | import androidx.lifecycle.SavedStateHandle 20 | import androidx.lifecycle.ViewModel 21 | import androidx.lifecycle.viewModelScope 22 | import com.wisemuji.zoomclone.model.MeetingOptions 23 | import com.wisemuji.zoomclone.ui.meetingroom.navigation.MeetingRoomRoute 24 | import dagger.hilt.android.lifecycle.HiltViewModel 25 | import io.getstream.video.android.core.Call 26 | import io.getstream.video.android.core.StreamVideo 27 | import kotlinx.coroutines.flow.MutableStateFlow 28 | import kotlinx.coroutines.flow.StateFlow 29 | import kotlinx.coroutines.launch 30 | import kotlinx.serialization.json.Json 31 | import javax.inject.Inject 32 | 33 | @HiltViewModel 34 | class MeetingRoomViewModel @Inject constructor( 35 | savedStateHandle: SavedStateHandle, 36 | ) : ViewModel() { 37 | private val meetingOptions = savedStateHandle 38 | .get(MeetingRoomRoute.ARGS_MEETING_OPTIONS) 39 | ?.let { Json.decodeFromString(it) } 40 | ?: error("MeetingOptions not found") 41 | 42 | private val _uiState = 43 | MutableStateFlow(MeetingUiState.Loading) 44 | val uiState: StateFlow = _uiState 45 | 46 | fun loadMeeting( 47 | type: String = DEFAULT_TYPE, 48 | meetingOptions: MeetingOptions = this.meetingOptions, 49 | ) { 50 | updateUsername(meetingOptions.username) 51 | val id = meetingOptions.meetingId 52 | val call = StreamVideo.instance().call(type = type, id = id) 53 | viewModelScope.launch { 54 | val result = call.join(create = true) 55 | result.onSuccess { 56 | call.applyMeetingOptions(meetingOptions) 57 | _uiState.value = MeetingUiState.Success(call) 58 | }.onError { 59 | _uiState.value = MeetingUiState.Error 60 | } 61 | } 62 | } 63 | 64 | private fun updateUsername(username: String) { 65 | if (username.isEmpty()) return 66 | // TODO: Update the user's name 67 | } 68 | 69 | private fun Call.applyMeetingOptions(meetingOptions: MeetingOptions) { 70 | camera.setEnabled(meetingOptions.videoOn) 71 | microphone.setEnabled(meetingOptions.audioOn) 72 | } 73 | 74 | companion object { 75 | private const val DEFAULT_TYPE = "default" 76 | } 77 | } 78 | 79 | sealed interface MeetingUiState { 80 | data object Loading : MeetingUiState 81 | data class Success(val call: Call) : MeetingUiState 82 | data object Error : MeetingUiState 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/meetingroom/ReactionItemData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.meetingroom 18 | 19 | import androidx.compose.foundation.clickable 20 | import androidx.compose.foundation.layout.Arrangement 21 | import androidx.compose.foundation.layout.Box 22 | import androidx.compose.foundation.layout.Column 23 | import androidx.compose.foundation.layout.Row 24 | import androidx.compose.foundation.layout.fillMaxWidth 25 | import androidx.compose.foundation.layout.padding 26 | import androidx.compose.foundation.layout.wrapContentWidth 27 | import androidx.compose.material3.Card 28 | import androidx.compose.material3.CardDefaults 29 | import androidx.compose.material3.MaterialTheme 30 | import androidx.compose.material3.Text 31 | import androidx.compose.runtime.Composable 32 | import androidx.compose.runtime.rememberCoroutineScope 33 | import androidx.compose.ui.Modifier 34 | import androidx.compose.ui.platform.LocalContext 35 | import androidx.compose.ui.text.style.TextAlign 36 | import androidx.compose.ui.tooling.preview.Preview 37 | import androidx.compose.ui.unit.dp 38 | import androidx.compose.ui.window.Dialog 39 | import com.wisemuji.zoomclone.ui.component.ZoomVideoTheme 40 | import com.wisemuji.zoomclone.ui.theme.ZoomCloneComposeTheme 41 | import io.getstream.video.android.core.Call 42 | import io.getstream.video.android.core.mapper.ReactionMapper 43 | import io.getstream.video.android.mock.StreamPreviewDataUtils 44 | import io.getstream.video.android.mock.previewCall 45 | import kotlinx.coroutines.CoroutineScope 46 | import kotlinx.coroutines.launch 47 | 48 | private data class ReactionItemData(val emojiDescription: String, val emojiCode: String) 49 | 50 | private object DefaultReactionsMenuData { 51 | val mainReaction = ReactionItemData("Raise hand", ":raise-hand:") 52 | val defaultReactions = listOf( 53 | ReactionItemData("Wave", ":hello:"), 54 | ReactionItemData("Like", ":raise-hand:"), 55 | ReactionItemData("Heart", ":heart:"), 56 | ReactionItemData("Joy", "😂"), 57 | ReactionItemData("Opened mouth", "😮"), 58 | ReactionItemData("Fireworks", ":fireworks:"), 59 | ) 60 | } 61 | 62 | @Composable 63 | internal fun ReactionsMenu( 64 | call: Call, 65 | reactionMapper: ReactionMapper, 66 | onDismiss: () -> Unit, 67 | ) { 68 | val scope = rememberCoroutineScope() 69 | val modifier = Modifier 70 | .wrapContentWidth() 71 | val onEmojiSelected: (emoji: String) -> Unit = { 72 | sendReaction(scope, call, it, onDismiss) 73 | } 74 | 75 | Dialog(onDismiss) { 76 | Card( 77 | colors = CardDefaults.cardColors().copy( 78 | containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, 79 | contentColor = MaterialTheme.colorScheme.inverseOnSurface, 80 | ), 81 | modifier = modifier.wrapContentWidth(), 82 | ) { 83 | Column(Modifier.padding(16.dp)) { 84 | Row(horizontalArrangement = Arrangement.Center) { 85 | ReactionItem( 86 | modifier = Modifier 87 | .fillMaxWidth(), 88 | textModifier = Modifier.fillMaxWidth(), 89 | reactionMapper = reactionMapper, 90 | reaction = DefaultReactionsMenuData.mainReaction, 91 | showDescription = true, 92 | onEmojiSelected = onEmojiSelected, 93 | ) 94 | } 95 | Row( 96 | horizontalArrangement = Arrangement.Center, 97 | ) { 98 | DefaultReactionsMenuData.defaultReactions.forEach { 99 | ReactionItem( 100 | modifier = modifier.weight(1f), 101 | reactionMapper = reactionMapper, 102 | onEmojiSelected = onEmojiSelected, 103 | reaction = it, 104 | ) 105 | } 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | @Composable 113 | private fun ReactionItem( 114 | modifier: Modifier = Modifier, 115 | textModifier: Modifier = Modifier, 116 | reactionMapper: ReactionMapper, 117 | reaction: ReactionItemData, 118 | onEmojiSelected: (emoji: String) -> Unit, 119 | showDescription: Boolean = false, 120 | ) { 121 | val mappedEmoji = reactionMapper.map(reaction.emojiCode) 122 | Box( 123 | modifier = modifier 124 | .clickable { 125 | onEmojiSelected(reaction.emojiCode) 126 | } 127 | .padding(2.dp), 128 | ) { 129 | Text( 130 | textAlign = TextAlign.Center, 131 | modifier = textModifier.padding(12.dp), 132 | text = "$mappedEmoji${if (showDescription) reaction.emojiDescription else ""}", 133 | ) 134 | } 135 | } 136 | 137 | private fun sendReaction(scope: CoroutineScope, call: Call, emoji: String, onDismiss: () -> Unit) { 138 | scope.launch { 139 | call.sendReaction("default", emoji) 140 | onDismiss() 141 | } 142 | } 143 | 144 | @Preview 145 | @Composable 146 | private fun ReactionMenuPreview() { 147 | ZoomCloneComposeTheme { 148 | ZoomVideoTheme { 149 | StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) 150 | ReactionsMenu( 151 | call = previewCall, 152 | reactionMapper = ReactionMapper.defaultReactionMapper(), 153 | onDismiss = { }, 154 | ) 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/meetingroom/ToggleButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.meetingroom 18 | 19 | import androidx.compose.material3.FilledIconButton 20 | import androidx.compose.material3.Icon 21 | import androidx.compose.material3.IconButtonDefaults 22 | import androidx.compose.material3.MaterialTheme 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.getValue 25 | import androidx.compose.ui.res.painterResource 26 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 27 | import com.wisemuji.zoomclone.R 28 | import io.getstream.video.android.core.Call 29 | 30 | @Composable 31 | fun ToggleMicrophoneButton(call: Call) { 32 | val isMicrophoneEnabled by call.microphone.isEnabled.collectAsStateWithLifecycle() 33 | ToggleButton( 34 | onClick = { call.microphone.setEnabled(!isMicrophoneEnabled) }, 35 | enabled = isMicrophoneEnabled, 36 | enabledIcon = R.drawable.ic_zoom_mic_on, 37 | disabledIcon = R.drawable.ic_zoom_mic_off, 38 | ) 39 | } 40 | 41 | @Composable 42 | fun ToggleCameraButton(call: Call) { 43 | val isCameraEnabled by call.camera.isEnabled.collectAsStateWithLifecycle() 44 | ToggleButton( 45 | onClick = { call.camera.setEnabled(!isCameraEnabled) }, 46 | enabled = isCameraEnabled, 47 | enabledIcon = R.drawable.ic_zoom_video_on, 48 | disabledIcon = R.drawable.ic_zoom_video_off, 49 | ) 50 | } 51 | 52 | @Composable 53 | fun ToggleReactionsButton(toggleReactions: () -> Unit) { 54 | ToggleButton( 55 | onClick = toggleReactions, 56 | enabled = true, 57 | enabledIcon = R.drawable.ic_zoom_reaction, 58 | ) 59 | } 60 | 61 | @Composable 62 | private fun ToggleButton( 63 | onClick: () -> Unit, 64 | enabled: Boolean, 65 | enabledIcon: Int, 66 | disabledIcon: Int = enabledIcon, 67 | ) { 68 | FilledIconButton( 69 | onClick = onClick, 70 | colors = IconButtonDefaults.filledIconButtonColors( 71 | containerColor = MaterialTheme.colorScheme.inverseSurface, 72 | ), 73 | ) { 74 | if (enabled) { 75 | Icon( 76 | painterResource(enabledIcon), 77 | contentDescription = "enable toggle", 78 | ) 79 | } else { 80 | Icon( 81 | painterResource(disabledIcon), 82 | contentDescription = "disable toggle", 83 | ) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/meetingroom/navigation/MeetingRoomNavigation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.meetingroom.navigation 18 | 19 | import androidx.navigation.NavController 20 | import androidx.navigation.NavGraphBuilder 21 | import androidx.navigation.NavType 22 | import androidx.navigation.compose.composable 23 | import androidx.navigation.navArgument 24 | import com.wisemuji.zoomclone.model.MeetingOptions 25 | import com.wisemuji.zoomclone.ui.meetingroom.MeetingRoomScreen 26 | import com.wisemuji.zoomclone.ui.meetingroom.navigation.MeetingRoomRoute.ARGS_MEETING_OPTIONS 27 | import com.wisemuji.zoomclone.ui.meetingroom.navigation.MeetingRoomRoute.ROUTE 28 | import kotlinx.serialization.encodeToString 29 | import kotlinx.serialization.json.Json 30 | 31 | fun NavController.navigateToMeetingRoom(meetingOptions: MeetingOptions) { 32 | val encoded = Json.encodeToString(meetingOptions) 33 | popBackStack() 34 | navigate(ROUTE.replace("{$ARGS_MEETING_OPTIONS}", encoded)) { 35 | launchSingleTop = true 36 | } 37 | } 38 | 39 | fun NavGraphBuilder.meetingRoomScreen( 40 | onBackPressed: () -> Unit, 41 | ) { 42 | composable( 43 | route = ROUTE, 44 | arguments = listOf( 45 | navArgument(ARGS_MEETING_OPTIONS) { type = NavType.StringType }, 46 | ), 47 | ) { 48 | MeetingRoomScreen(onBackPressed) 49 | } 50 | } 51 | 52 | object MeetingRoomRoute { 53 | const val ARGS_MEETING_OPTIONS = "meeting_options" 54 | const val ROUTE = "meeting_room/{${ARGS_MEETING_OPTIONS}}" 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/newmeeting/NewMeetingScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.newmeeting 18 | 19 | import androidx.compose.foundation.background 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.foundation.layout.Column 22 | import androidx.compose.foundation.layout.Spacer 23 | import androidx.compose.foundation.layout.fillMaxSize 24 | import androidx.compose.foundation.layout.fillMaxWidth 25 | import androidx.compose.foundation.layout.height 26 | import androidx.compose.foundation.layout.padding 27 | import androidx.compose.material3.MaterialTheme 28 | import androidx.compose.material3.Scaffold 29 | import androidx.compose.material3.SnackbarHost 30 | import androidx.compose.material3.SnackbarHostState 31 | import androidx.compose.material3.Text 32 | import androidx.compose.material3.TextButton 33 | import androidx.compose.runtime.Composable 34 | import androidx.compose.runtime.LaunchedEffect 35 | import androidx.compose.runtime.getValue 36 | import androidx.compose.runtime.mutableStateOf 37 | import androidx.compose.runtime.remember 38 | import androidx.compose.runtime.setValue 39 | import androidx.compose.ui.Alignment.Companion.Center 40 | import androidx.compose.ui.Alignment.Companion.CenterStart 41 | import androidx.compose.ui.Modifier 42 | import androidx.compose.ui.res.stringResource 43 | import androidx.compose.ui.text.font.FontWeight.Companion.Medium 44 | import androidx.compose.ui.tooling.preview.Preview 45 | import androidx.compose.ui.unit.dp 46 | import androidx.compose.ui.unit.sp 47 | import com.wisemuji.zoomclone.R 48 | import com.wisemuji.zoomclone.model.MeetingOptions 49 | import com.wisemuji.zoomclone.ui.component.DefaultHorizontalDivider 50 | import com.wisemuji.zoomclone.ui.component.StatusBarColor 51 | import com.wisemuji.zoomclone.ui.component.ZoomFullSizeButton 52 | import com.wisemuji.zoomclone.ui.component.ZoomSwitchRow 53 | import com.wisemuji.zoomclone.ui.theme.ZoomCloneComposeTheme 54 | 55 | @Composable 56 | fun NewMeetingScreen( 57 | onBackPressed: () -> Unit, 58 | onJoinMeetingClick: (MeetingOptions) -> Unit, 59 | ) { 60 | val snackbarHostState = remember { SnackbarHostState() } 61 | var showNotImplementedSnackbar by remember { mutableStateOf(false) } 62 | var videoOn by remember { mutableStateOf(true) } 63 | 64 | if (showNotImplementedSnackbar) { 65 | LaunchedEffect(snackbarHostState) { 66 | snackbarHostState.showSnackbar("Not implemented yet. Do you want to contribute? :)") 67 | showNotImplementedSnackbar = false 68 | } 69 | } 70 | StatusBarColor(color = MaterialTheme.colorScheme.surface, isIconLight = true) 71 | Scaffold( 72 | snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, 73 | topBar = { NewMeetingTopAppBar(onBack = onBackPressed) }, 74 | ) { innerPadding -> 75 | Column( 76 | modifier = Modifier 77 | .padding(innerPadding) 78 | .fillMaxSize() 79 | .background(MaterialTheme.colorScheme.surfaceContainer), 80 | ) { 81 | DefaultHorizontalDivider() 82 | Spacer(modifier = Modifier.padding(12.dp)) 83 | DefaultHorizontalDivider() 84 | ZoomSwitchRow( 85 | title = stringResource(R.string.video_on), 86 | checked = videoOn, 87 | onCheckedChange = { videoOn = it }, 88 | ) 89 | DefaultHorizontalDivider() 90 | ZoomSwitchRow( 91 | title = stringResource(R.string.use_personal_meeting_id), 92 | subtitle = stringResource(R.string.personal_meeting_id), 93 | checked = false, 94 | onCheckedChange = { showNotImplementedSnackbar = true }, 95 | ) 96 | DefaultHorizontalDivider() 97 | ZoomFullSizeButton( 98 | text = stringResource(R.string.new_meeting_title), 99 | onClick = { onJoinMeetingClick(MeetingOptions(videoOn = videoOn)) }, 100 | modifier = Modifier.padding(24.dp), 101 | ) 102 | } 103 | } 104 | } 105 | 106 | @Composable 107 | private fun NewMeetingTopAppBar(onBack: () -> Unit) { 108 | Box( 109 | contentAlignment = CenterStart, 110 | modifier = Modifier 111 | .fillMaxWidth() 112 | .height(56.dp) 113 | .padding(horizontal = 4.dp), 114 | ) { 115 | TextButton(onClick = onBack) { 116 | Text(text = stringResource(R.string.cancel), fontSize = 17.sp) 117 | } 118 | Text( 119 | text = stringResource(R.string.new_meeting_title), 120 | fontSize = 17.sp, 121 | fontWeight = Medium, 122 | modifier = Modifier.align(Center), 123 | ) 124 | } 125 | } 126 | 127 | @Preview 128 | @Composable 129 | private fun NewMeetingScreenPreview() { 130 | ZoomCloneComposeTheme { 131 | NewMeetingScreen({}, {}) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/newmeeting/navigation/NewMeetingNavigation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.newmeeting.navigation 18 | 19 | import androidx.navigation.NavController 20 | import androidx.navigation.NavGraphBuilder 21 | import androidx.navigation.compose.composable 22 | import com.wisemuji.zoomclone.model.MeetingOptions 23 | import com.wisemuji.zoomclone.ui.newmeeting.NewMeetingScreen 24 | import com.wisemuji.zoomclone.ui.newmeeting.navigation.NewMeetingRoute.ROUTE 25 | 26 | fun NavController.navigateToNewMeeting() { 27 | navigate(ROUTE) 28 | } 29 | 30 | fun NavGraphBuilder.newMeetingScreen( 31 | onBackPressed: () -> Unit, 32 | onJoinMeetingClick: (MeetingOptions) -> Unit, 33 | ) { 34 | composable(route = ROUTE) { 35 | NewMeetingScreen(onBackPressed, onJoinMeetingClick) 36 | } 37 | } 38 | 39 | object NewMeetingRoute { 40 | const val ROUTE = "new_meeting" 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.theme 18 | 19 | import androidx.compose.ui.graphics.Color 20 | 21 | val Orange = Color(0xFFFF742F) 22 | val Blue = Color(0xFF0E72EC) 23 | val Red = Color(0xFFDE2827) 24 | val Green = Color(0xFF4FD963) 25 | 26 | val Black = Color(0xFF000000) 27 | val Gray80 = Color(0xFF38394D) 28 | val Gray60 = Color(0xFF717075) 29 | val Gray50 = Color(0xFF929197) 30 | val Gray30 = Color(0xFFE4E4E4) 31 | val Gray20 = Color(0xFFEBEAEF) 32 | val Gray10 = Color(0xFFF9F9F9) 33 | val White = Color(0xFFFFFFFF) 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.theme 18 | 19 | import android.app.Activity 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.lightColorScheme 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.runtime.SideEffect 24 | import androidx.compose.ui.graphics.toArgb 25 | import androidx.compose.ui.platform.LocalView 26 | import androidx.core.view.WindowCompat 27 | 28 | private val LightColorScheme = lightColorScheme( 29 | primary = Blue, 30 | onPrimary = White, 31 | secondary = Orange, 32 | onSecondary = White, 33 | tertiary = Green, 34 | onTertiary = White, 35 | background = White, 36 | onBackground = Black, 37 | surface = White, 38 | onSurface = Black, 39 | onSurfaceVariant = Gray80, 40 | surfaceContainer = Gray10, 41 | surfaceVariant = White, 42 | surfaceContainerHighest = Gray80, 43 | inverseSurface = Black, 44 | inverseOnSurface = White, 45 | outlineVariant = Gray30, 46 | ) 47 | 48 | // TODO: Add dark color scheme 49 | private val DarkColorScheme = LightColorScheme 50 | 51 | @Composable 52 | fun ZoomCloneComposeTheme( 53 | darkTheme: Boolean = false, 54 | content: @Composable () -> Unit, 55 | ) { 56 | val colorScheme = when { 57 | darkTheme -> DarkColorScheme 58 | else -> LightColorScheme 59 | } 60 | val view = LocalView.current 61 | if (!view.isInEditMode) { 62 | SideEffect { 63 | val window = (view.context as Activity).window 64 | window.statusBarColor = colorScheme.primary.toArgb() 65 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 66 | } 67 | } 68 | 69 | MaterialTheme( 70 | colorScheme = colorScheme, 71 | typography = Typography, 72 | content = content, 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/wisemuji/zoomclone/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Suhyeon(wisemuji) and Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.wisemuji.zoomclone.ui.theme 18 | 19 | import androidx.compose.material3.Typography 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.text.font.FontFamily 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.unit.sp 24 | 25 | // Set of Material typography styles to start with 26 | val Typography = Typography( 27 | bodyLarge = TextStyle( 28 | fontFamily = FontFamily.Default, 29 | fontWeight = FontWeight.Normal, 30 | fontSize = 16.sp, 31 | lineHeight = 24.sp, 32 | letterSpacing = 0.5.sp, 33 | ), 34 | /* Other default text styles to override 35 | titleLarge = TextStyle( 36 | fontFamily = FontFamily.Default, 37 | fontWeight = FontWeight.Normal, 38 | fontSize = 22.sp, 39 | lineHeight = 28.sp, 40 | letterSpacing = 0.sp 41 | ), 42 | labelSmall = TextStyle( 43 | fontFamily = FontFamily.Default, 44 | fontWeight = FontWeight.Medium, 45 | fontSize = 11.sp, 46 | lineHeight = 16.sp, 47 | letterSpacing = 0.5.sp 48 | ) 49 | */ 50 | ) 51 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zoom_breakout.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zoom_chat.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zoom_join_meeting.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zoom_mic_off.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zoom_mic_on.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zoom_participants.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zoom_reaction.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zoom_video_off.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_zoom_video_on.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisemuji/zoom-clone-compose/890f44f2d2954746c15cf796fec928c753e0e1da/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisemuji/zoom-clone-compose/890f44f2d2954746c15cf796fec928c753e0e1da/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisemuji/zoom-clone-compose/890f44f2d2954746c15cf796fec928c753e0e1da/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisemuji/zoom-clone-compose/890f44f2d2954746c15cf796fec928c753e0e1da/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisemuji/zoom-clone-compose/890f44f2d2954746c15cf796fec928c753e0e1da/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisemuji/zoom-clone-compose/890f44f2d2954746c15cf796fec928c753e0e1da/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisemuji/zoom-clone-compose/890f44f2d2954746c15cf796fec928c753e0e1da/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisemuji/zoom-clone-compose/890f44f2d2954746c15cf796fec928c753e0e1da/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisemuji/zoom-clone-compose/890f44f2d2954746c15cf796fec928c753e0e1da/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisemuji/zoom-clone-compose/890f44f2d2954746c15cf796fec928c753e0e1da/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ZoomCloneCompose 3 | 4 | Meetings 5 | New Meeting 6 | Join 7 | WIP 8 | 9 | Start a Meeting 10 | Cancel 11 | Video on 12 | Use personal meeting ID (PMI) 13 | XXX XXX XXXX 14 | 15 | Join a Meeting 16 | Meeting ID 17 | Your Name 18 | Join 19 | If you received an invitation link, tap on the link to join the meeting 20 | Join options 21 | Don\'t Connect To Audio 22 | Turn Off My Video 23 | 24 | Zoom Clone 25 | Connecting… 26 | Oops! Something went wrong. 27 | 28 | About 29 | This is a Zoom clone app built with Stream Video SDK for Compose to implement real-time video meeting features using Jetpack Compose. 30 | OK 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |