├── .editorconfig ├── .github ├── CODEOWNERS ├── pull_request_template.md └── workflows │ └── android.yml ├── .gitignore ├── .kotlin └── errors │ └── errors-1728956286992.log ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── fabric.properties ├── lint-baseline.xml ├── proguard-common.txt ├── proguard-rules.pro ├── proguard-specific.txt └── src │ ├── main │ ├── AndroidManifest.xml │ ├── baseline-prof.txt │ ├── java │ │ └── io │ │ │ └── getstream │ │ │ └── slackclone │ │ │ ├── SlackCloneApp.kt │ │ │ ├── di │ │ │ └── NavigationModule.kt │ │ │ └── root │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable │ │ ├── ic_circle.xml │ │ └── slack.png │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── splash_theme.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── resources │ └── responses │ └── jokes_response.json ├── art ├── architecture.png ├── art0.gif ├── art1.png ├── art2.png ├── art3.png ├── art4.png ├── art5.png ├── art6.png ├── art7.png └── art8.png ├── benchmark ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── io │ └── getstream │ └── slackclone │ └── benchmark │ ├── baseprofile │ └── BaselineProfileGenerator.kt │ └── startup │ └── StartupBenchmark.kt ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── Dependencies.kt │ └── ProjectProperties.kt ├── common ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── io │ │ └── getstream │ │ └── slackclone │ │ └── common │ │ ├── extensions │ │ └── PrimitiveExtensions.kt │ │ ├── injection │ │ ├── DispatcherModule.kt │ │ └── dispatcher │ │ │ ├── CoroutineDispatcherProvider.kt │ │ │ └── RealCoroutineDispatcherProvider.kt │ │ └── startup │ │ └── TimberInitializer.kt │ └── res │ └── values │ └── strings.xml ├── commonui ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── io │ │ └── getstream │ │ └── slackclone │ │ └── commonui │ │ ├── keyboard │ │ └── Keyboard.kt │ │ ├── material │ │ └── SlackSurfaceAppBar.kt │ │ ├── reusable │ │ ├── SlackDragComposableView.kt │ │ ├── SlackImageBox.kt │ │ └── SlackListItem.kt │ │ └── theme │ │ ├── Color.kt │ │ ├── PraxisSurface.kt │ │ ├── Shape.kt │ │ ├── Theme.kt │ │ └── Type.kt │ └── res │ ├── anim │ ├── slide_left_in.xml │ ├── slide_left_out.xml │ ├── slide_right_in.xml │ └── slide_right_out.xml │ ├── drawable │ ├── ic_email.xml │ ├── ic_eye.xml │ ├── logo_compose.png │ ├── logo_flutter.png │ ├── logo_gdg.jpeg │ ├── logo_kotlin.png │ └── logo_stream.png │ ├── font │ ├── lato_bold.ttf │ ├── lato_light.ttf │ └── lato_regular.ttf │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-v21 │ └── styles.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── integer.xml │ ├── strings.xml │ └── styles.xml ├── data ├── .gitignore ├── build.gradle.kts └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── getstream │ │ └── slackclone │ │ └── data │ │ └── ExampleInstrumentedTest.kt │ └── main │ ├── AndroidManifest.xml │ └── java │ └── io │ └── getstream │ └── slackclone │ └── data │ ├── injection │ ├── ChatModule.kt │ ├── DataMappersModule.kt │ ├── DataModule.kt │ ├── RepositoryModule.kt │ └── UseCaseModule.kt │ ├── local │ ├── SlackDatabase.kt │ ├── dao │ │ ├── SlackChannelDao.kt │ │ └── SlackUserDao.kt │ └── model │ │ ├── DBSlackChannel.kt │ │ └── DBSlackUser.kt │ ├── mapper │ ├── EntityMapper.kt │ ├── SlackChannelMapper.kt │ ├── SlackUserChannelMapper.kt │ └── SlackUserMapper.kt │ └── repository │ ├── SlackChannelsRepositoryImpl.kt │ └── SlackUserRepository.kt ├── domain ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ └── io │ └── getstream │ └── slackclone │ └── domain │ ├── mappers │ └── UiModelMapper.kt │ ├── model │ ├── channel │ │ └── DomainLayerChannels.kt │ ├── login │ │ └── LoginState.kt │ ├── message │ │ ├── ChannelMessage.kt │ │ └── DomainLayerMessages.kt │ └── users │ │ └── DomainLayerUsers.kt │ ├── repository │ ├── ChannelsRepository.kt │ └── UsersRepository.kt │ └── usecases │ ├── BaseUseCase.kt │ ├── channels │ ├── UseCaseCreateLocalChannel.kt │ ├── UseCaseCreateLocalChannels.kt │ ├── UseCaseCreateRemoteChannel.kt │ ├── UseCaseFetchChannelCount.kt │ ├── UseCaseFetchChannels.kt │ ├── UseCaseGetChannel.kt │ ├── UseCaseSearchChannel.kt │ └── UseCaseSendMessageToChannel.kt │ └── users │ ├── UseCaseFetchUsers.kt │ ├── UseCaseLoginUser.kt │ └── UseCaseLogoutUser.kt ├── feat-channels ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── io │ │ └── getstream │ │ └── slackclone │ │ └── uichannels │ │ ├── SlackChannelVM.kt │ │ ├── createsearch │ │ ├── CreateChannelVM.kt │ │ ├── CreateNewChannelUI.kt │ │ ├── SearchChannelsVM.kt │ │ └── SearchCreateChannelsScreen.kt │ │ ├── directmessages │ │ ├── DMChannelsList.kt │ │ └── DMessageViewModel.kt │ │ └── views │ │ ├── SKExpandCollapseColumn.kt │ │ ├── SlackAllChannels.kt │ │ ├── SlackConnections.kt │ │ ├── SlackDirectMessages.kt │ │ ├── SlackRecentChannels.kt │ │ └── SlackStarredChannels.kt │ └── res │ └── values │ └── strings.xml ├── feat-chat ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── io │ │ └── getstream │ │ └── slackclone │ │ └── uichat │ │ ├── chatthread │ │ ├── ChatScreenUI.kt │ │ ├── ChatScreenVM.kt │ │ └── composables │ │ │ ├── ChatMessage.kt │ │ │ ├── ChatMessageBox.kt │ │ │ ├── ChatMessageDateSeparator.kt │ │ │ ├── ChatMessageReactionSelectMenu.kt │ │ │ ├── ChatMessagesUI.kt │ │ │ ├── ChatScreenContent.kt │ │ │ ├── ChatSystemMessage.kt │ │ │ ├── MessageListEmptyContent.kt │ │ │ └── reactions │ │ │ ├── ChatMessageReactionOptionItem.kt │ │ │ ├── SlackCloneReactionFactory.kt │ │ │ ├── SlackCloneReactions.kt │ │ │ └── SlackMessageReactionItem.kt │ │ └── newchat │ │ ├── NewChatThreadScreen.kt │ │ └── NewChatThreadVM.kt │ └── res │ ├── drawable │ ├── joy.png │ ├── love.png │ ├── smile.png │ ├── thumbsup.png │ └── wink.png │ └── values │ └── strings.xml ├── feat-chatcore ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── io │ └── getstream │ └── slackclone │ └── chatcore │ ├── ChannelUIModelMapper.kt │ ├── data │ ├── ExpandCollapseModel.kt │ └── UiLayerChannels.kt │ ├── extensions │ └── ModelExtensions.kt │ ├── injection │ ├── UiModelMapperModule.kt │ └── UserChannelUiMapper.kt │ ├── startup │ └── StreamChatInitializer.kt │ └── views │ └── SlackChannelItem.kt ├── feat-onboarding ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── io │ │ └── getstream │ │ └── slackclone │ │ └── uionboarding │ │ ├── compose │ │ ├── CommonInputUI.kt │ │ ├── EmailInputView.kt │ │ ├── GettingStarted.kt │ │ ├── OnBoardingVM.kt │ │ ├── ScreenInputUI.kt │ │ ├── SkipTypingScreen.kt │ │ └── WorkspaceInputView.kt │ │ └── nav │ │ └── OnboardingNavigation.kt │ └── res │ ├── drawable-hdpi │ └── gettingstarted.png │ ├── drawable-xxhdpi │ └── gettingstarted.png │ ├── drawable-xxxhdpi │ └── gettingstarted.png │ └── values │ └── strings.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── navigator ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── io │ └── getstream │ └── slackclone │ └── navigator │ ├── NavigationCommand.kt │ ├── NavigationKeys.kt │ ├── Navigator.kt │ ├── Screens.kt │ └── SlackCloneComposeNavigator.kt ├── settings.gradle.kts ├── spotless.gradle.kts ├── team-props ├── git-hooks.gradle.kts └── git-hooks │ └── pre-commit.sh └── ui-dashboard ├── .gitignore ├── build.gradle.kts └── src └── main ├── AndroidManifest.xml ├── java └── io │ └── getstream │ └── slackclone │ └── uidashboard │ ├── compose │ ├── DashboardUI.kt │ ├── DashboardVM.kt │ └── SideNavigation.kt │ ├── home │ ├── DirectMessagesUI.kt │ ├── HomeScreenUI.kt │ ├── MentionsReactionsUI.kt │ ├── SearchMessagesUI.kt │ ├── UserProfileUI.kt │ └── search │ │ └── SearchCancel.kt │ └── nav │ └── dashboardNavigation.kt └── res └── values └── strings.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | # Most of the standard properties are supported 4 | indent_size=2 5 | max_line_length=100 -------------------------------------------------------------------------------- /.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 | * @skydoves -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Guidelines 2 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 3 | 4 | ### Types of changes 5 | What types of changes does your code introduce? 6 | 7 | - [ ] Bugfix (non-breaking change which fixes an issue) 8 | - [ ] New feature (non-breaking change which adds functionality) 9 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 10 | 11 | ### Preparing a pull request for review 12 | Ensure your change is properly formatted by running: 13 | 14 | ```gradle 15 | $ ./gradlew spotlessApply 16 | ``` 17 | 18 | Please correct any failures before requesting a review. -------------------------------------------------------------------------------- /.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 | java-version: 17 19 | 20 | - name: Cache Gradle and wrapper 21 | uses: actions/cache@v2 22 | with: 23 | path: | 24 | ~/.gradle/caches 25 | ~/.gradle/wrapper 26 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} 27 | restore-keys: | 28 | ${{ runner.os }}-gradle- 29 | - name: Make Gradle executable 30 | run: chmod +x ./gradlew 31 | 32 | - name: Build with Gradle 33 | run: ./gradlew build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /captures 8 | .externalNativeBuild 9 | .idea 10 | *.aab 11 | .cxx 12 | */build 13 | */.gradle 14 | /buildSrc/build 15 | build -------------------------------------------------------------------------------- /.kotlin/errors/errors-1728956286992.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.0.21 2 | error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: 3 | 1. Kotlin compile daemon is ready 4 | 5 | -------------------------------------------------------------------------------- /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 | ## Preparing a pull request for review 5 | Ensure your change is properly formatted by running: 6 | 7 | ```gradle 8 | ./gradlew spotlessApply 9 | ``` 10 | 11 | Please correct any failures before requesting a review. 12 | 13 | ## Code reviews 14 | 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. 15 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Manifest version information! 2 | 3 | plugins { 4 | id(BuildPlugins.ANDROID_APPLICATION_PLUGIN) 5 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 6 | id(BuildPlugins.KOTLIN_PARCELABLE_PLUGIN) 7 | id(BuildPlugins.KOTLIN_KSP) 8 | id(BuildPlugins.COMPOSE_COMPILER) 9 | id(BuildPlugins.DAGGER_HILT) 10 | } 11 | 12 | android { 13 | compileSdk = (ProjectProperties.COMPILE_SDK) 14 | namespace = "io.getstream.slackclone" 15 | 16 | defaultConfig { 17 | applicationId = (ProjectProperties.APPLICATION_ID) 18 | minSdk = (ProjectProperties.MIN_SDK) 19 | targetSdk = (ProjectProperties.TARGET_SDK) 20 | versionCode = 1 21 | versionName = "1.0" 22 | testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" 23 | vectorDrawables.useSupportLibrary = true 24 | } 25 | 26 | buildTypes { 27 | val debug by getting { 28 | isDebuggable = true 29 | versionNameSuffix = "-debug" 30 | applicationIdSuffix = ".debug" 31 | } 32 | 33 | val release by getting { 34 | isDebuggable = false 35 | versionNameSuffix = "-release" 36 | isMinifyEnabled = true 37 | isShrinkResources = true 38 | 39 | proguardFiles( 40 | getDefaultProguardFile("proguard-android.txt"), "proguard-common.txt", 41 | "proguard-specific.txt" 42 | ) 43 | } 44 | 45 | val benchmark by creating { 46 | initWith(release) 47 | signingConfig = signingConfigs.getByName("debug") 48 | matchingFallbacks.add("release") 49 | proguardFiles("benchmark-rules.pro") 50 | } 51 | } 52 | 53 | lint { 54 | abortOnError = false 55 | } 56 | 57 | buildFeatures { 58 | compose = true 59 | } 60 | 61 | packagingOptions { 62 | resources.excludes.add("META-INF/LICENSE.txt") 63 | resources.excludes.add("META-INF/NOTICE.txt") 64 | resources.excludes.add("LICENSE.txt") 65 | resources.excludes.add( "/META-INF/{AL2.0,LGPL2.1}") 66 | } 67 | 68 | compileOptions { 69 | sourceCompatibility = JavaVersion.VERSION_1_8 70 | targetCompatibility = JavaVersion.VERSION_1_8 71 | } 72 | 73 | kotlinOptions { 74 | jvmTarget = "1.8" 75 | } 76 | } 77 | 78 | dependencies { 79 | api(project(":feat-onboarding")) 80 | api(project(":ui-dashboard")) 81 | api(project(":feat-chat")) 82 | 83 | implementation(project(":navigator")) 84 | implementation(project(":data")) 85 | implementation(project(":domain")) 86 | implementation(project(":common")) 87 | implementation(project(":commonui")) 88 | 89 | /* Android Designing and layout */ 90 | implementation(Lib.Android.COMPOSE_LIVEDATA) 91 | implementation(Lib.Android.COMPOSE_NAVIGATION) 92 | implementation(Lib.Kotlin.KT_STD) 93 | implementation(Lib.Android.MATERIAL_DESIGN) 94 | implementation(Lib.Android.CONSTRAINT_LAYOUT_COMPOSE) 95 | implementation(Lib.Android.SPLASH_SCREEN_API) 96 | 97 | implementation(Lib.Android.APP_COMPAT) 98 | 99 | implementation(Lib.Kotlin.KTX_CORE) 100 | 101 | /*DI*/ 102 | implementation(Lib.Di.hiltAndroid) 103 | implementation(Lib.Di.hiltNavigationCompose) 104 | 105 | ksp(Lib.Di.hiltCompiler) 106 | ksp(Lib.Di.hiltAndroidCompiler) 107 | 108 | /* Logger */ 109 | implementation(Lib.Logger.TIMBER) 110 | /* Async */ 111 | implementation(Lib.Async.COROUTINES) 112 | implementation(Lib.Async.COROUTINES_ANDROID) 113 | 114 | /* Room */ 115 | implementation(Lib.Room.roomRuntime) 116 | ksp(Lib.Room.roomCompiler) 117 | implementation(Lib.Room.roomKtx) 118 | implementation(Lib.Room.roomPaging) 119 | 120 | /*Testing*/ 121 | testImplementation(TestLib.JUNIT) 122 | testImplementation(TestLib.CORE_TEST) 123 | testImplementation(TestLib.ANDROID_JUNIT) 124 | testImplementation(TestLib.ARCH_CORE) 125 | testImplementation(TestLib.MOCK_WEB_SERVER) 126 | testImplementation(TestLib.ROBO_ELECTRIC) 127 | testImplementation(TestLib.COROUTINES) 128 | testImplementation(TestLib.MOCKK) 129 | 130 | androidTestImplementation("androidx.compose.ui:ui-test-junit4:${Lib.Android.COMPOSE_VERSION}") 131 | debugImplementation("androidx.compose.ui:ui-test-manifest:${Lib.Android.COMPOSE_VERSION}") 132 | } 133 | -------------------------------------------------------------------------------- /app/fabric.properties: -------------------------------------------------------------------------------- 1 | #Contains API Secret used to validate your application. Commit to internal source control; avoid making secret public. 2 | #Wed Mar 08 18:34:26 IST 2017 3 | apiSecret=9cb50ff88ceece358871f08a6424267dd88f84ca25ecce34d12d8c1d9ff12367 4 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/Development/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -keep class com.github.vatbub.** { *; } 19 | -keepclassmembers class com.github.vatbub.** { *; } 20 | -------------------------------------------------------------------------------- /app/proguard-specific.txt: -------------------------------------------------------------------------------- 1 | #MM Proguard Settings pertaining to this project 2 | # this is an extension to the recommended settings for android 3 | # provide in proguard-android.pro 4 | # 5 | # It is also an extention to the proguard configuration of SlackClone 6 | # 7 | # Add proguard directives to this file if this project requires additional 8 | # configuration 9 | 10 | -keepnames !abstract class com.customername.android.injection.* 11 | 12 | #Keeping the members of that have static vars 13 | -keepclassmembers public class com.customername.android.** { 14 | public static * ; 15 | public *; 16 | } 17 | 18 | # Below will be classes you want to explicity keep AND obfuscate - you shouldn't need to do this unless your class is only referenced at runtime and not compile time (IE injected via annotation or reflection) 19 | #-keep,allowobfuscation class com.customername.android.** { *; } 20 | 21 | #Things you don't want to obfuscate and you don't want to be shrunk usually GSON pojos. Add your domain/JSON below here 22 | -keep class com.customername.android.model.** { *; } 23 | 24 | -dontwarn okio.** 25 | -dontwarn org.simpleframework.** 26 | -keep class com.google.common.** { *; } 27 | 28 | 29 | #Rxjava 30 | -keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { 31 | long producerIndex; 32 | long consumerIndex; 33 | } 34 | 35 | -keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef { 36 | rx.internal.util.atomic.LinkedQueueNode producerNode; 37 | } 38 | 39 | -keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef { 40 | rx.internal.util.atomic.LinkedQueueNode consumerNode; 41 | } 42 | 43 | 44 | # Retrofit2 45 | -dontwarn retrofit2.** 46 | -keep class retrofit2.** { *; } 47 | -keepattributes Signature 48 | -keepattributes Exceptions 49 | -keepattributes Annotation 50 | 51 | -dontwarn android.databinding.** 52 | -keep class android.databinding.** { *; } 53 | -dontwarn com.google.errorprone.annotations.** 54 | 55 | 56 | -keep class okhttp3.** { *; } 57 | -keep interface okhttp3.** { *; } 58 | -dontwarn okhttp3.** 59 | -dontwarn okio.** 60 | -dontwarn javax.annotation** 61 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/io/getstream/slackclone/SlackCloneApp.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class SlackCloneApp : Application() 8 | -------------------------------------------------------------------------------- /app/src/main/java/io/getstream/slackclone/di/NavigationModule.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.di 2 | 3 | import dagger.Binds 4 | import dagger.Module 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import io.getstream.slackclone.navigator.ComposeNavigator 8 | import io.getstream.slackclone.navigator.SlackCloneComposeNavigator 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | abstract class NavigationModule { 14 | 15 | @Binds 16 | @Singleton 17 | abstract fun provideComposeNavigator(slackCloneComposeNavigator: SlackCloneComposeNavigator): ComposeNavigator 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/getstream/slackclone/root/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.root 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.runtime.LaunchedEffect 7 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 8 | import androidx.core.view.WindowCompat 9 | import androidx.navigation.compose.NavHost 10 | import androidx.navigation.compose.rememberNavController 11 | import dagger.hilt.android.AndroidEntryPoint 12 | import io.getstream.slackclone.navigator.ComposeNavigator 13 | import io.getstream.slackclone.navigator.SlackRoute 14 | import io.getstream.slackclone.uidashboard.nav.dashboardNavigation 15 | import io.getstream.slackclone.uionboarding.nav.onboardingNavigation 16 | import javax.inject.Inject 17 | 18 | @AndroidEntryPoint 19 | class MainActivity : ComponentActivity() { 20 | 21 | @Inject 22 | lateinit var composeNavigator: ComposeNavigator 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | WindowCompat.setDecorFitsSystemWindows(window, false) 27 | 28 | installSplashScreen() 29 | setContent { 30 | val navController = rememberNavController() 31 | 32 | LaunchedEffect(Unit) { 33 | composeNavigator.handleNavigationCommands(navController) 34 | } 35 | NavHost( 36 | navController = navController, 37 | startDestination = SlackRoute.OnBoarding.name, 38 | ) { 39 | onboardingNavigation( 40 | composeNavigator = composeNavigator, 41 | ) 42 | dashboardNavigation( 43 | composeNavigator = composeNavigator 44 | ) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_circle.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/app/src/main/res/drawable/slack.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/splash_theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MainActivity 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /commonui/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | #ffffff 7 | #009688 8 | #0645AD 9 | #000 10 | #411540 11 | #e4e4e2 12 | #838381 13 | 14 | 15 | -------------------------------------------------------------------------------- /commonui/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20dp 4 | 16sp 5 | 10sp 6 | 26sp 7 | 8 | 5dp 9 | 8dp 10 | 10dp 11 | 16dp 12 | 65dp 13 | 120dp 14 | 15 | -------------------------------------------------------------------------------- /commonui/src/main/res/values/integer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 300 4 | -------------------------------------------------------------------------------- /commonui/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SlackClone 3 | 4 | -------------------------------------------------------------------------------- /commonui/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 17 | 18 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_KSP) 5 | id(BuildPlugins.DAGGER_HILT) 6 | } 7 | 8 | android { 9 | compileSdk = ProjectProperties.COMPILE_SDK 10 | namespace = "io.getstream.slackclone.data" 11 | 12 | defaultConfig { 13 | minSdk = (ProjectProperties.MIN_SDK) 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | 17 | buildTypes { 18 | getByName("release") { 19 | isMinifyEnabled = false 20 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | 27 | implementation("com.github.vatbub:randomusers:1.3"){ 28 | exclude("com.google.guava") 29 | } 30 | implementation(project(":common")) 31 | implementation(project(":domain")) 32 | 33 | /*Stream Chat SDK*/ 34 | api(Lib.STREAM.STREAM_CHAT_CLIENT) 35 | 36 | /*Kotlin*/ 37 | api(Lib.Kotlin.KT_STD) 38 | api(Lib.Async.COROUTINES) 39 | 40 | /* Paging */ 41 | implementation(Lib.Paging.PAGING_3) 42 | /* Room */ 43 | api(Lib.Room.roomRuntime) 44 | ksp(Lib.Room.roomCompiler) 45 | api(Lib.Room.roomKtx) 46 | api(Lib.Room.roomPaging) 47 | 48 | /* Networking */ 49 | api(Lib.Networking.RETROFIT) 50 | api(Lib.Networking.RETROFIT_GSON) 51 | api(Lib.Networking.LOGGING) 52 | 53 | api(Lib.Serialization.GSON) 54 | 55 | /* Dependency Injection */ 56 | api(Lib.Di.hiltAndroid) 57 | ksp(Lib.Di.hiltAndroidCompiler) 58 | } -------------------------------------------------------------------------------- /data/src/androidTest/java/io/getstream/slackclone/data/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data 2 | 3 | /** 4 | * Instrumented test, which will execute on an Android device. 5 | * 6 | * See [testing documentation](http://d.android.com/tools/testing). 7 | */ 8 | class ExampleInstrumentedTest { 9 | fun useAppContext() { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/injection/ChatModule.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.injection 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import io.getstream.chat.android.client.ChatClient 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | @InstallIn(SingletonComponent::class) 12 | object ChatModule { 13 | 14 | @Provides 15 | @Singleton 16 | fun provideStreamChatClient() = ChatClient.instance() 17 | } 18 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/injection/DataMappersModule.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.injection 2 | 3 | import com.github.vatbub.randomusers.result.RandomUser 4 | import dagger.Binds 5 | import dagger.Module 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import io.getstream.slackclone.data.local.model.DBSlackChannel 9 | import io.getstream.slackclone.data.mapper.EntityMapper 10 | import io.getstream.slackclone.data.mapper.SlackChannelMapper 11 | import io.getstream.slackclone.data.mapper.SlackUserChannelMapper 12 | import io.getstream.slackclone.data.mapper.SlackUserMapper 13 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 14 | import io.getstream.slackclone.domain.model.users.DomainLayerUsers 15 | import javax.inject.Singleton 16 | 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | abstract class DataMappersModule { 20 | 21 | @Binds 22 | @Singleton 23 | abstract fun bindSlackUserChannelMapper(slackUserChannelMapper: SlackUserChannelMapper): EntityMapper 24 | 25 | @Binds 26 | @Singleton 27 | abstract fun bindSlackUserDataDomainMapper(slackUserMapper: SlackUserMapper): EntityMapper 28 | 29 | @Binds 30 | @Singleton 31 | abstract fun bindSlackChannelDataDomainMapper(slackChannelMapper: SlackChannelMapper): EntityMapper 32 | } 33 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/injection/DataModule.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.injection 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import dagger.hilt.components.SingletonComponent 10 | import io.getstream.slackclone.data.local.SlackDatabase 11 | import io.getstream.slackclone.data.local.dao.SlackChannelDao 12 | import io.getstream.slackclone.data.local.dao.SlackUserDao 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | object DataModule { 18 | @Provides 19 | @Singleton 20 | fun provideDatabase(@ApplicationContext context: Context): SlackDatabase { 21 | return Room.inMemoryDatabaseBuilder( 22 | context, 23 | SlackDatabase::class.java, 24 | ).fallbackToDestructiveMigration().allowMainThreadQueries().build() 25 | } 26 | 27 | @Provides 28 | @Singleton 29 | fun provideChannelDao(slackDatabase: SlackDatabase): SlackChannelDao = 30 | slackDatabase.slackChannelDao() 31 | 32 | @Provides 33 | @Singleton 34 | fun provideUserDao(slackDatabase: SlackDatabase): SlackUserDao = slackDatabase.slackUserDao() 35 | } 36 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/injection/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.injection 2 | 3 | import dagger.Binds 4 | import dagger.Module 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import io.getstream.slackclone.data.repository.SlackChannelsRepositoryImpl 8 | import io.getstream.slackclone.data.repository.SlackUserRepository 9 | import io.getstream.slackclone.domain.repository.ChannelsRepository 10 | import io.getstream.slackclone.domain.repository.UsersRepository 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | abstract class RepositoryModule { 16 | @Binds 17 | @Singleton 18 | abstract fun bindLocalChannelsRepository(slackLocalChannelsRepositoryImpl: SlackChannelsRepositoryImpl): ChannelsRepository 19 | 20 | @Binds 21 | @Singleton 22 | abstract fun bindSlackUserRepository(slackUserRepository: SlackUserRepository): UsersRepository 23 | } 24 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/injection/UseCaseModule.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.injection 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.android.components.ViewModelComponent 7 | import dagger.hilt.android.scopes.ViewModelScoped 8 | import io.getstream.slackclone.domain.repository.ChannelsRepository 9 | import io.getstream.slackclone.domain.repository.UsersRepository 10 | import io.getstream.slackclone.domain.usecases.channels.UseCaseCreateLocalChannel 11 | import io.getstream.slackclone.domain.usecases.channels.UseCaseCreateLocalChannels 12 | import io.getstream.slackclone.domain.usecases.channels.UseCaseCreateRemoteChannel 13 | import io.getstream.slackclone.domain.usecases.channels.UseCaseFetchChannelCount 14 | import io.getstream.slackclone.domain.usecases.channels.UseCaseFetchChannels 15 | import io.getstream.slackclone.domain.usecases.channels.UseCaseGetChannel 16 | import io.getstream.slackclone.domain.usecases.channels.UseCaseSearchChannel 17 | import io.getstream.slackclone.domain.usecases.channels.UseCaseSendMessageToChannel 18 | import io.getstream.slackclone.domain.usecases.users.UseCaseFetchUsers 19 | import io.getstream.slackclone.domain.usecases.users.UseCaseLoginUser 20 | import io.getstream.slackclone.domain.usecases.users.UseCaseLogoutUser 21 | 22 | @Module 23 | @InstallIn(ViewModelComponent::class) 24 | object UseCaseModule { 25 | 26 | @Provides 27 | @ViewModelScoped 28 | fun provideUseCaseFetchChannels(channelsRepository: ChannelsRepository) = 29 | UseCaseFetchChannels(channelsRepository) 30 | 31 | @Provides 32 | @ViewModelScoped 33 | fun provideUseCaseCreateLocalChannel(channelsRepository: ChannelsRepository) = 34 | UseCaseCreateLocalChannel(channelsRepository) 35 | 36 | @Provides 37 | @ViewModelScoped 38 | fun provideUseCaseCreateLocalChannels(channelsRepository: ChannelsRepository) = 39 | UseCaseCreateLocalChannels(channelsRepository) 40 | 41 | @Provides 42 | @ViewModelScoped 43 | fun provideUseCaseCreateRemoteChannel(channelsRepository: ChannelsRepository) = 44 | UseCaseCreateRemoteChannel(channelsRepository) 45 | 46 | @Provides 47 | @ViewModelScoped 48 | fun provideUseCaseSendMessageToChannel(channelsRepository: ChannelsRepository) = 49 | UseCaseSendMessageToChannel(channelsRepository) 50 | 51 | @Provides 52 | @ViewModelScoped 53 | fun provideUseCaseGetChannel(channelsRepository: ChannelsRepository) = 54 | UseCaseGetChannel(channelsRepository) 55 | 56 | @Provides 57 | @ViewModelScoped 58 | fun provideUseCaseFetchChannelCount(channelsRepository: ChannelsRepository) = 59 | UseCaseFetchChannelCount(channelsRepository) 60 | 61 | @Provides 62 | @ViewModelScoped 63 | fun provideUseCaseSearchChannel(channelsRepository: ChannelsRepository) = 64 | UseCaseSearchChannel(channelsRepository) 65 | 66 | @Provides 67 | @ViewModelScoped 68 | fun provideUseCaseFetchUsers(slackUsersRepository: UsersRepository) = 69 | UseCaseFetchUsers(slackUsersRepository) 70 | 71 | @Provides 72 | @ViewModelScoped 73 | fun provideUseCaseLogoutUser(slackUsersRepository: UsersRepository) = 74 | UseCaseLogoutUser(slackUsersRepository) 75 | 76 | @Provides 77 | @ViewModelScoped 78 | fun provideUseCaseLoginUser(slackUsersRepository: UsersRepository) = 79 | UseCaseLoginUser(slackUsersRepository) 80 | } 81 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/local/SlackDatabase.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.local 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import io.getstream.slackclone.data.local.dao.SlackChannelDao 6 | import io.getstream.slackclone.data.local.dao.SlackUserDao 7 | import io.getstream.slackclone.data.local.model.DBSlackChannel 8 | import io.getstream.slackclone.data.local.model.DBSlackUser 9 | 10 | @Database( 11 | entities = [DBSlackUser::class, DBSlackChannel::class], 12 | version = 1 13 | ) 14 | abstract class SlackDatabase : RoomDatabase() { 15 | abstract fun slackUserDao(): SlackUserDao 16 | abstract fun slackChannelDao(): SlackChannelDao 17 | } 18 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/local/dao/SlackChannelDao.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.local.dao 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.room.Dao 5 | import androidx.room.Delete 6 | import androidx.room.Insert 7 | import androidx.room.OnConflictStrategy 8 | import androidx.room.Query 9 | import io.getstream.slackclone.data.local.model.DBSlackChannel 10 | import kotlinx.coroutines.flow.Flow 11 | 12 | @Dao 13 | interface SlackChannelDao { 14 | 15 | @Query("SELECT COUNT(*) from slackChannel ") 16 | fun count(): Int 17 | 18 | @Query("SELECT * FROM slackChannel") 19 | fun getAll(): List 20 | 21 | @Query("SELECT * FROM slackChannel") 22 | fun getAllAsFlow(): Flow> 23 | 24 | @Query("SELECT * FROM slackChannel WHERE uuid IN (:groupIds)") 25 | fun loadAllByIds(groupIds: Array): List 26 | 27 | @Query( 28 | "SELECT * FROM slackChannel WHERE name LIKE :name" 29 | ) 30 | fun findByName(name: String): List 31 | 32 | @Insert(onConflict = OnConflictStrategy.REPLACE) 33 | fun insertAll(channelDB: List) 34 | 35 | @Insert(onConflict = OnConflictStrategy.REPLACE) 36 | suspend fun insert(channel: DBSlackChannel) 37 | 38 | @Delete 39 | fun delete(channelDB: DBSlackChannel) 40 | 41 | @Query("SELECT * from slackChannel where uuid like :uuid") 42 | fun getById(uuid: String): DBSlackChannel? 43 | 44 | // The Int type parameter tells Room to use a PositionalDataSource object. 45 | @Query("SELECT * FROM slackChannel where name like '%' || :params || '%' ORDER BY name ASC") 46 | fun channelsByName(params: String?): PagingSource 47 | 48 | // The Int type parameter tells Room to use a PositionalDataSource object. 49 | @Query("SELECT * FROM slackChannel ORDER BY name ASC") 50 | fun channelsByName(): PagingSource 51 | } 52 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/local/dao/SlackUserDao.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.local.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.Query 7 | import io.getstream.slackclone.data.local.model.DBSlackUser 8 | 9 | @Dao 10 | interface SlackUserDao { 11 | @Query("SELECT * FROM slackuser") 12 | fun getAll(): List 13 | 14 | @Query("SELECT * FROM slackuser WHERE uuid IN (:slackuserIds)") 15 | fun loadAllByIds(slackuserIds: IntArray): List 16 | 17 | @Query( 18 | "SELECT * FROM slackuser WHERE first_name LIKE :first AND " + 19 | "last_name LIKE :last LIMIT 1" 20 | ) 21 | fun findByName(first: String, last: String): DBSlackUser 22 | 23 | @Insert 24 | fun insertAll(slackUsers: List) 25 | 26 | @Delete 27 | fun delete(slackUser: DBSlackUser) 28 | } 29 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/local/model/DBSlackChannel.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.local.model 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "slackChannel") 8 | data class DBSlackChannel( 9 | @PrimaryKey val uuid: String, 10 | @ColumnInfo(name = "name") val name: String?, 11 | @ColumnInfo(name = "createdDate") val createdDate: Long? = System.currentTimeMillis(), 12 | @ColumnInfo(name = "modifiedDate") val modifiedDate: Long? = System.currentTimeMillis(), 13 | @ColumnInfo(name = "isMuted") val isMuted: Boolean? = false, 14 | @ColumnInfo(name = "isStarred") val isStarred: Boolean? = false, 15 | @ColumnInfo(name = "isPrivate") val isPrivate: Boolean? = false, 16 | @ColumnInfo(name = "isShareOutSide") val isShareOutSide: Boolean? = false, 17 | @ColumnInfo(name = "photo") val avatarUrl: String? = null, 18 | @ColumnInfo(name = "isOneToOne") val isOneToOne: Boolean? = null 19 | ) 20 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/local/model/DBSlackUser.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.local.model 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "slackUser") 8 | data class DBSlackUser( 9 | @PrimaryKey val uuid: String, 10 | @ColumnInfo(name = "first_name") val firstName: String?, 11 | @ColumnInfo(name = "last_name") val lastName: String?, 12 | @ColumnInfo(name = "photo") val photo: String? 13 | ) 14 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/mapper/EntityMapper.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.mapper 2 | 3 | interface EntityMapper { 4 | fun mapToDomain(entity: Data): Domain 5 | 6 | fun mapToData(model: Domain): Data 7 | } 8 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/mapper/SlackChannelMapper.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.mapper 2 | 3 | import io.getstream.slackclone.data.local.model.DBSlackChannel 4 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 5 | import javax.inject.Inject 6 | 7 | class SlackChannelMapper @Inject constructor() : 8 | EntityMapper { 9 | override fun mapToDomain(entity: DBSlackChannel): DomainLayerChannels.SlackChannel { 10 | return DomainLayerChannels.SlackChannel( 11 | isStarred = entity.isStarred, 12 | isPrivate = entity.isPrivate, 13 | uuid = entity.uuid, 14 | name = entity.name, 15 | isMuted = entity.isMuted, 16 | createdDate = entity.createdDate, 17 | modifiedDate = entity.modifiedDate, 18 | isShareOutSide = entity.isShareOutSide, 19 | isOneToOne = entity.isOneToOne, 20 | avatarUrl = entity.avatarUrl 21 | ) 22 | } 23 | 24 | override fun mapToData(model: DomainLayerChannels.SlackChannel): DBSlackChannel { 25 | return DBSlackChannel( 26 | model.uuid ?: model.name!!, 27 | model.name, 28 | isStarred = model.isStarred, 29 | createdDate = model.createdDate, 30 | modifiedDate = model.modifiedDate, 31 | isPrivate = model.isPrivate, 32 | isShareOutSide = model.isShareOutSide, 33 | isOneToOne = model.isOneToOne, avatarUrl = model.avatarUrl 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/mapper/SlackUserChannelMapper.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.mapper 2 | 3 | import io.getstream.slackclone.data.local.model.DBSlackChannel 4 | import io.getstream.slackclone.domain.model.users.DomainLayerUsers 5 | import javax.inject.Inject 6 | 7 | class SlackUserChannelMapper @Inject constructor() : 8 | EntityMapper { 9 | override fun mapToDomain(entity: DBSlackChannel): DomainLayerUsers.SlackUser { 10 | TODO("Not yet implemented") 11 | } 12 | 13 | override fun mapToData(model: DomainLayerUsers.SlackUser): DBSlackChannel { 14 | return DBSlackChannel(model.login, model.name, avatarUrl = model.picture, isOneToOne = true) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/mapper/SlackUserMapper.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.mapper 2 | 3 | import com.github.vatbub.randomusers.result.Name 4 | import com.github.vatbub.randomusers.result.RandomUser 5 | import io.getstream.slackclone.domain.model.users.DomainLayerUsers 6 | import javax.inject.Inject 7 | 8 | class SlackUserMapper @Inject constructor() : EntityMapper { 9 | override fun mapToDomain(entity: RandomUser): DomainLayerUsers.SlackUser { 10 | return DomainLayerUsers.SlackUser( 11 | entity.gender.genderText, 12 | entity.name.fullName(), 13 | entity.location.city, 14 | entity.email, 15 | entity.login.username, 16 | entity.dateOfBirth.time, 17 | entity.registrationDate.time, 18 | entity.phone, 19 | entity.cell, 20 | entity.picture.mediumPicture.toURI().toString(), 21 | entity.nationality.shortCode 22 | ) 23 | } 24 | 25 | override fun mapToData(model: DomainLayerUsers.SlackUser): RandomUser { 26 | TODO("not needed!") 27 | } 28 | } 29 | 30 | private fun Name.fullName(): String { 31 | return "${this.firstName} ${this.lastName}" 32 | } 33 | -------------------------------------------------------------------------------- /data/src/main/java/io/getstream/slackclone/data/repository/SlackUserRepository.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.data.repository 2 | 3 | import com.github.vatbub.randomusers.Generator 4 | import com.github.vatbub.randomusers.result.RandomUser 5 | import io.getstream.chat.android.client.ChatClient 6 | import io.getstream.chat.android.models.User 7 | import io.getstream.slackclone.common.injection.dispatcher.CoroutineDispatcherProvider 8 | import io.getstream.slackclone.data.mapper.EntityMapper 9 | import io.getstream.slackclone.domain.model.login.LoginState 10 | import io.getstream.slackclone.domain.model.users.DomainLayerUsers 11 | import io.getstream.slackclone.domain.repository.UsersRepository 12 | import kotlinx.coroutines.withContext 13 | import javax.inject.Inject 14 | 15 | class SlackUserRepository @Inject constructor( 16 | private val mapper: EntityMapper, 17 | private val coroutineMainDispatcherProvider: CoroutineDispatcherProvider, 18 | private val chatClient: ChatClient 19 | ) : UsersRepository { 20 | 21 | override suspend fun getUsers(count: Int): List { 22 | return withContext(coroutineMainDispatcherProvider.io) { 23 | Generator.generateRandomUsers( 24 | RandomUser.RandomUserSpec(), 25 | count 26 | ).map { 27 | mapper.mapToDomain(it) 28 | } 29 | } 30 | } 31 | 32 | override suspend fun login(userName: String): LoginState { 33 | val name: String = if (userName.contains("@")) { 34 | userName.split("@")[0] 35 | } else { 36 | userName 37 | } 38 | val user = User( 39 | id = name, 40 | name = name, 41 | image = "http://placekitten.com/200/300" 42 | ) 43 | val token = chatClient.devToken(user.id) 44 | val result = chatClient.connectUser(user = user, token = token).await() 45 | return if (result.isSuccess) { 46 | LoginState.Success 47 | } else { 48 | LoginState.Failure(result.errorOrNull()?.message.orEmpty()) 49 | } 50 | } 51 | 52 | override suspend fun logout() { 53 | chatClient.disconnect(flushPersistence = true) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("org.jetbrains.kotlin.jvm") 4 | } 5 | 6 | java { 7 | sourceCompatibility = JavaVersion.VERSION_1_8 8 | targetCompatibility = JavaVersion.VERSION_1_8 9 | } 10 | 11 | dependencies { 12 | api(Lib.Kotlin.KT_STD) 13 | api(Lib.Async.COROUTINES) 14 | implementation("androidx.paging:paging-common-ktx:3.1.0") 15 | } 16 | 17 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/mappers/UiModelMapper.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.mappers 2 | 3 | interface UiModelMapper { 4 | fun mapToPresentation(model: DomainModel): UiModel 5 | 6 | fun mapToDomain(modelItem: UiModel): DomainModel 7 | } 8 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/model/channel/DomainLayerChannels.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.model.channel 2 | 3 | interface DomainLayerChannels { 4 | data class SlackChannel( 5 | val uuid: String? = null, 6 | val name: String? = null, 7 | val createdDate: Long? = null, 8 | val modifiedDate: Long? = null, 9 | val isMuted: Boolean? = null, 10 | val isPrivate: Boolean? = null, 11 | val isStarred: Boolean? = false, 12 | val isShareOutSide: Boolean? = false, 13 | val isOneToOne: Boolean?, 14 | val avatarUrl: String? 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/model/login/LoginState.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.model.login 2 | 3 | sealed class LoginState { 4 | object Nothing : LoginState() 5 | object Loading : LoginState() 6 | object Success : LoginState() 7 | class Failure(val message: String) : LoginState() 8 | } 9 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/model/message/ChannelMessage.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.model.message 2 | 3 | data class ChannelMessage( 4 | val cid: String, 5 | val message: String 6 | ) 7 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/model/message/DomainLayerMessages.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.model.message 2 | 3 | interface DomainLayerMessages { 4 | data class SlackMessage( 5 | val uuid: String, 6 | val channelId: String, 7 | val message: String, 8 | val userId: String, 9 | val createdBy: String, 10 | val createdDate: Long, 11 | val modifiedDate: Long, 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/model/users/DomainLayerUsers.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.model.users 2 | 3 | interface DomainLayerUsers { 4 | data class SlackUser( 5 | val gender: String, 6 | val name: String, 7 | val location: String, 8 | val email: String, 9 | val login: String, 10 | val dateOfBirth: Long, 11 | val registrationDate: Long, 12 | val phone: String, 13 | val cell: String, 14 | val picture: String, 15 | val nationality: String, 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/repository/ChannelsRepository.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.repository 2 | 3 | import androidx.paging.PagingData 4 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 5 | import io.getstream.slackclone.domain.model.message.ChannelMessage 6 | import io.getstream.slackclone.domain.model.users.DomainLayerUsers 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | interface ChannelsRepository { 10 | fun fetchChannels(): Flow> 11 | fun fetchChannelsPaged(params: String?): Flow> 12 | suspend fun createChannel(domainLayerChannels: DomainLayerChannels.SlackChannel) 13 | suspend fun sendMessageToChannel(channelMessage: ChannelMessage) 14 | suspend fun saveChannel(params: DomainLayerChannels.SlackChannel): DomainLayerChannels.SlackChannel? 15 | suspend fun getChannel(uuid: String): DomainLayerChannels.SlackChannel? 16 | suspend fun channelCount(): Int 17 | suspend fun saveOneToOneChannels(params: List) 18 | } 19 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/repository/UsersRepository.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.repository 2 | 3 | import io.getstream.slackclone.domain.model.login.LoginState 4 | import io.getstream.slackclone.domain.model.users.DomainLayerUsers 5 | 6 | interface UsersRepository { 7 | suspend fun getUsers(count: Int): List 8 | suspend fun login(userName: String): LoginState 9 | suspend fun logout() 10 | } 11 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/usecases/BaseUseCase.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.usecases 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | interface BaseUseCase { 6 | 7 | /** 8 | * Perform an operation with no input parameters. 9 | * Will throw an exception by default, if not implemented but invoked. 10 | * 11 | * @return 12 | */ 13 | suspend fun perform(): Result = throw NotImplementedError() 14 | 15 | /** 16 | * Perform an operation. 17 | * Will throw an exception by default, if not implemented but invoked. 18 | * 19 | * @param params 20 | * @return 21 | */ 22 | suspend fun perform(params: ExecutableParam): Result? = throw NotImplementedError() 23 | 24 | fun performStreaming(params: ExecutableParam?): Flow = throw NotImplementedError() 25 | } 26 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/usecases/channels/UseCaseCreateLocalChannel.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.usecases.channels 2 | 3 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 4 | import io.getstream.slackclone.domain.repository.ChannelsRepository 5 | import io.getstream.slackclone.domain.usecases.BaseUseCase 6 | 7 | class UseCaseCreateLocalChannel(private val channelsRepository: ChannelsRepository) : 8 | BaseUseCase { 9 | override suspend fun perform(params: DomainLayerChannels.SlackChannel): DomainLayerChannels.SlackChannel? { 10 | return channelsRepository.saveChannel(params) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/usecases/channels/UseCaseCreateLocalChannels.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.usecases.channels 2 | 3 | import io.getstream.slackclone.domain.model.users.DomainLayerUsers 4 | import io.getstream.slackclone.domain.repository.ChannelsRepository 5 | import io.getstream.slackclone.domain.usecases.BaseUseCase 6 | 7 | class UseCaseCreateLocalChannels(private val channelsRepository: ChannelsRepository) : 8 | BaseUseCase> { 9 | override suspend fun perform(params: List) { 10 | return channelsRepository.saveOneToOneChannels(params) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/usecases/channels/UseCaseCreateRemoteChannel.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.usecases.channels 2 | 3 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 4 | import io.getstream.slackclone.domain.repository.ChannelsRepository 5 | import io.getstream.slackclone.domain.usecases.BaseUseCase 6 | 7 | class UseCaseCreateRemoteChannel(private val channelsRepository: ChannelsRepository) : 8 | BaseUseCase { 9 | override suspend fun perform(params: DomainLayerChannels.SlackChannel) { 10 | channelsRepository.createChannel(params) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/usecases/channels/UseCaseFetchChannelCount.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.usecases.channels 2 | 3 | import io.getstream.slackclone.domain.repository.ChannelsRepository 4 | import io.getstream.slackclone.domain.usecases.BaseUseCase 5 | 6 | class UseCaseFetchChannelCount(private val channelsRepository: ChannelsRepository) : 7 | BaseUseCase { 8 | 9 | override suspend fun perform(): Int { 10 | return channelsRepository.channelCount() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/usecases/channels/UseCaseFetchChannels.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.usecases.channels 2 | 3 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 4 | import io.getstream.slackclone.domain.repository.ChannelsRepository 5 | import io.getstream.slackclone.domain.usecases.BaseUseCase 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | class UseCaseFetchChannels( 9 | private val channelsRepository: ChannelsRepository, 10 | ) : BaseUseCase, Unit> { 11 | 12 | override fun performStreaming(params: Unit?): Flow> { 13 | return channelsRepository.fetchChannels() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/usecases/channels/UseCaseGetChannel.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.usecases.channels 2 | 3 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 4 | import io.getstream.slackclone.domain.repository.ChannelsRepository 5 | import io.getstream.slackclone.domain.usecases.BaseUseCase 6 | 7 | class UseCaseGetChannel(private val channelsRepository: ChannelsRepository) : 8 | BaseUseCase { 9 | override suspend fun perform(params: String): DomainLayerChannels.SlackChannel? { 10 | return channelsRepository.getChannel(uuid = params) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/usecases/channels/UseCaseSearchChannel.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.usecases.channels 2 | 3 | import androidx.paging.PagingData 4 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 5 | import io.getstream.slackclone.domain.repository.ChannelsRepository 6 | import io.getstream.slackclone.domain.usecases.BaseUseCase 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | class UseCaseSearchChannel(private val channelsRepository: ChannelsRepository) : 10 | BaseUseCase, String> { 11 | override fun performStreaming(params: String?): Flow> { 12 | return channelsRepository.fetchChannelsPaged(params) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/usecases/channels/UseCaseSendMessageToChannel.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.usecases.channels 2 | 3 | import io.getstream.slackclone.domain.model.message.ChannelMessage 4 | import io.getstream.slackclone.domain.repository.ChannelsRepository 5 | import io.getstream.slackclone.domain.usecases.BaseUseCase 6 | 7 | class UseCaseSendMessageToChannel(private val channelsRepository: ChannelsRepository) : 8 | BaseUseCase { 9 | override suspend fun perform(params: ChannelMessage) { 10 | channelsRepository.sendMessageToChannel(params) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/usecases/users/UseCaseFetchUsers.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.usecases.users 2 | 3 | import io.getstream.slackclone.domain.model.users.DomainLayerUsers 4 | import io.getstream.slackclone.domain.repository.UsersRepository 5 | import io.getstream.slackclone.domain.usecases.BaseUseCase 6 | 7 | class UseCaseFetchUsers(private val usersRepository: UsersRepository) : 8 | BaseUseCase, Int> { 9 | override suspend fun perform(params: Int): List { 10 | return usersRepository.getUsers(params) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/usecases/users/UseCaseLoginUser.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.usecases.users 2 | 3 | import io.getstream.slackclone.domain.model.login.LoginState 4 | import io.getstream.slackclone.domain.repository.UsersRepository 5 | import io.getstream.slackclone.domain.usecases.BaseUseCase 6 | 7 | class UseCaseLoginUser(private val usersRepository: UsersRepository) : 8 | BaseUseCase { 9 | 10 | override suspend fun perform(params: String): LoginState { 11 | return usersRepository.login(params) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /domain/src/main/java/io/getstream/slackclone/domain/usecases/users/UseCaseLogoutUser.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.domain.usecases.users 2 | 3 | import io.getstream.slackclone.domain.repository.UsersRepository 4 | import io.getstream.slackclone.domain.usecases.BaseUseCase 5 | 6 | class UseCaseLogoutUser(private val usersRepository: UsersRepository) : 7 | BaseUseCase { 8 | override suspend fun perform() { 9 | usersRepository.logout() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /feat-channels/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feat-channels/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_KSP) 5 | id(BuildPlugins.DAGGER_HILT) 6 | id(BuildPlugins.COMPOSE_COMPILER) 7 | id(BuildPlugins.KOTLIN_PARCELABLE_PLUGIN) 8 | } 9 | 10 | android { 11 | compileSdk = ProjectProperties.COMPILE_SDK 12 | namespace = "io.getstream.slackclone.iochannels" 13 | 14 | defaultConfig { 15 | minSdk = (ProjectProperties.MIN_SDK) 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | getByName("release") { 21 | isMinifyEnabled = false 22 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 23 | } 24 | } 25 | 26 | buildFeatures { 27 | compose = true 28 | } 29 | 30 | packaging { 31 | resources.excludes.add("META-INF/LICENSE.txt") 32 | resources.excludes.add("META-INF/NOTICE.txt") 33 | resources.excludes.add("LICENSE.txt") 34 | resources.excludes.add("/META-INF/{AL2.0,LGPL2.1}") 35 | } 36 | 37 | compileOptions { 38 | sourceCompatibility = JavaVersion.VERSION_1_8 39 | targetCompatibility = JavaVersion.VERSION_1_8 40 | } 41 | 42 | kotlinOptions { 43 | jvmTarget = "1.8" 44 | } 45 | } 46 | 47 | dependencies { 48 | /*Kotlin*/ 49 | implementation(project(":data")) 50 | implementation(project(":domain")) 51 | implementation(project(":common")) 52 | implementation(project(":navigator")) 53 | implementation(project(":commonui")) 54 | implementation(project(":feat-chatcore")) 55 | 56 | api(Lib.Android.COMPOSE_UI) 57 | api(Lib.Android.LANDSCAPIST_GLIDE) 58 | api(Lib.Android.COMPOSE_MATERIAL) 59 | implementation(Lib.Android.ACCOMPANIST_SYSTEM_UI_CONTROLLER) 60 | api(Lib.Android.COMPOSE_UI) 61 | api(Lib.Android.COMPOSE_TOOLING) 62 | debugApi(Lib.Android.COMPOSE_DEBUG_TOOLING) 63 | api(Lib.Android.ACTIVITY_COMPOSE) 64 | api(Lib.Android.CONSTRAINT_LAYOUT_COMPOSE) 65 | implementation(Lib.Paging.PAGING_3) 66 | implementation(Lib.Paging.PAGING_COMPOSE) 67 | 68 | api(Lib.Android.APP_COMPAT) 69 | api(Lib.Kotlin.KTX_CORE) 70 | 71 | /*DI*/ 72 | api(Lib.Di.hiltAndroid) 73 | api(Lib.Di.hiltNavigationCompose) 74 | 75 | ksp(Lib.Di.hiltCompiler) 76 | ksp(Lib.Di.hiltAndroidCompiler) 77 | 78 | /* Logger */ 79 | api(Lib.Logger.TIMBER) 80 | /* Async */ 81 | api(Lib.Async.COROUTINES) 82 | api(Lib.Async.COROUTINES_ANDROID) 83 | 84 | testImplementation(TestLib.JUNIT) 85 | testImplementation(TestLib.CORE_TEST) 86 | testImplementation(TestLib.ANDROID_JUNIT) 87 | testImplementation(TestLib.ARCH_CORE) 88 | testImplementation(TestLib.MOCK_WEB_SERVER) 89 | testImplementation(TestLib.ROBO_ELECTRIC) 90 | testImplementation(TestLib.COROUTINES) 91 | testImplementation(TestLib.MOCKK) 92 | testImplementation(TestLib.TURBINE) 93 | } -------------------------------------------------------------------------------- /feat-channels/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feat-channels/src/main/java/io/getstream/slackclone/uichannels/SlackChannelVM.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichannels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.hilt.android.lifecycle.HiltViewModel 5 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 6 | import io.getstream.slackclone.domain.mappers.UiModelMapper 7 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 8 | import io.getstream.slackclone.domain.usecases.channels.UseCaseFetchChannels 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.emptyFlow 12 | import kotlinx.coroutines.flow.map 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class SlackChannelVM @Inject constructor( 17 | private val ucFetchChannels: UseCaseFetchChannels, 18 | private val chatPresentationMapper: UiModelMapper 19 | ) : ViewModel() { 20 | 21 | val channels = MutableStateFlow>>(emptyFlow()) 22 | 23 | fun allChannels() { 24 | channels.value = ucFetchChannels.performStreaming(null).map { channels -> 25 | domSlackToPresentation(channels) 26 | } 27 | } 28 | 29 | fun loadDirectMessageChannels() { 30 | channels.value = ucFetchChannels.performStreaming(null).map { channels -> 31 | domSlackToPresentation(channels) 32 | } 33 | } 34 | 35 | fun loadStarredChannels() { 36 | channels.value = ucFetchChannels.performStreaming(null).map { channels -> 37 | domSlackToPresentation(channels) 38 | } 39 | } 40 | 41 | private fun domSlackToPresentation(channels: List) = 42 | channels.map { channel -> 43 | chatPresentationMapper.mapToPresentation(channel) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /feat-channels/src/main/java/io/getstream/slackclone/uichannels/createsearch/CreateChannelVM.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichannels.createsearch 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 7 | import io.getstream.slackclone.domain.usecases.channels.UseCaseCreateLocalChannel 8 | import io.getstream.slackclone.navigator.ComposeNavigator 9 | import io.getstream.slackclone.navigator.NavigationKeys 10 | import io.getstream.slackclone.navigator.SlackScreen 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class CreateChannelVM @Inject constructor( 17 | private val composeNavigator: ComposeNavigator, 18 | private val useCaseCreateChannel: UseCaseCreateLocalChannel 19 | ) : 20 | ViewModel() { 21 | 22 | val channel = 23 | MutableStateFlow(DomainLayerChannels.SlackChannel(isOneToOne = false, avatarUrl = null)) 24 | 25 | fun createChannel() { 26 | viewModelScope.launch { 27 | if (channel.value.name?.isNotEmpty() == true) { 28 | val channel = useCaseCreateChannel.perform(channel.value) 29 | composeNavigator.navigateBackWithResult( 30 | NavigationKeys.navigateChannel, 31 | channel?.uuid!!, 32 | SlackScreen.Dashboard.name 33 | ) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /feat-channels/src/main/java/io/getstream/slackclone/uichannels/createsearch/SearchChannelsVM.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichannels.createsearch 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import androidx.paging.map 6 | import dagger.hilt.android.lifecycle.HiltViewModel 7 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 8 | import io.getstream.slackclone.domain.mappers.UiModelMapper 9 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 10 | import io.getstream.slackclone.domain.usecases.channels.UseCaseFetchChannelCount 11 | import io.getstream.slackclone.domain.usecases.channels.UseCaseSearchChannel 12 | import io.getstream.slackclone.navigator.ComposeNavigator 13 | import io.getstream.slackclone.navigator.NavigationKeys 14 | import io.getstream.slackclone.navigator.SlackScreen 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.flow.map 17 | import kotlinx.coroutines.launch 18 | import javax.inject.Inject 19 | 20 | @HiltViewModel 21 | class SearchChannelsVM @Inject constructor( 22 | private val composeNavigator: ComposeNavigator, 23 | private val ucFetchChannels: UseCaseSearchChannel, 24 | private val useCaseFetchChannelCount: UseCaseFetchChannelCount, 25 | private val chatPresentationMapper: UiModelMapper 26 | ) : ViewModel() { 27 | 28 | val search = MutableStateFlow("") 29 | val channelCount = MutableStateFlow(0) 30 | val channels = MutableStateFlow(flow("")) 31 | 32 | init { 33 | viewModelScope.launch { 34 | val count = useCaseFetchChannelCount.perform() 35 | channelCount.value = count 36 | } 37 | } 38 | 39 | private fun flow(search: String) = ucFetchChannels.performStreaming(search).map { channels -> 40 | channels.map { channel -> 41 | chatPresentationMapper.mapToPresentation(channel) 42 | } 43 | } 44 | 45 | fun search(newValue: String) { 46 | search.value = newValue 47 | channels.value = flow(newValue) 48 | } 49 | 50 | fun navigate(channel: UiLayerChannels.SlackChannel) { 51 | composeNavigator.navigateBackWithResult( 52 | NavigationKeys.navigateChannel, 53 | channel.uuid!!, 54 | SlackScreen.Dashboard.name 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /feat-channels/src/main/java/io/getstream/slackclone/uichannels/directmessages/DMChannelsList.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichannels.directmessages 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.rememberLazyListState 11 | import androidx.compose.material.Icon 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.LaunchedEffect 15 | import androidx.compose.runtime.collectAsState 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.painter.Painter 20 | import androidx.compose.ui.res.painterResource 21 | import androidx.compose.ui.res.stringResource 22 | import androidx.compose.ui.text.style.TextAlign 23 | import androidx.compose.ui.unit.dp 24 | import androidx.hilt.navigation.compose.hiltViewModel 25 | import io.getstream.chat.android.compose.ui.theme.ChatTheme 26 | import io.getstream.chat.android.models.Channel 27 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 28 | import io.getstream.slackclone.chatcore.extensions.toSlackDomainChannelMessage 29 | import io.getstream.slackclone.chatcore.extensions.toSlackUIChannel 30 | import io.getstream.slackclone.chatcore.views.DMLastMessageItem 31 | import io.getstream.slackclone.commonui.theme.SlackCloneColorProvider 32 | 33 | @Composable 34 | fun DMChannelsList( 35 | onItemClick: (UiLayerChannels.SlackChannel) -> Unit, 36 | channelVM: DMessageViewModel = hiltViewModel() 37 | ) { 38 | 39 | val channels by channelVM.channels.collectAsState() 40 | val listState = rememberLazyListState() 41 | 42 | LaunchedEffect(key1 = Unit) { 43 | channelVM.refresh() 44 | } 45 | 46 | if (channels.isNotEmpty()) { 47 | LazyColumn(state = listState) { 48 | for (index in channels.indices) { 49 | val channel: Channel = channels[index] 50 | 51 | item { 52 | DMLastMessageItem({ 53 | onItemClick(it.toSlackUIChannel()) 54 | }, channel, channel.toSlackDomainChannelMessage()) 55 | } 56 | } 57 | } 58 | } else { 59 | ChatTheme { 60 | EmptyContent( 61 | modifier = Modifier.fillMaxSize(), 62 | painter = painterResource(id = io.getstream.chat.android.compose.R.drawable.stream_compose_empty_channels), 63 | text = stringResource(io.getstream.chat.android.compose.R.string.stream_compose_channel_list_empty_channels), 64 | ) 65 | } 66 | } 67 | } 68 | 69 | @Composable 70 | fun EmptyContent( 71 | text: String, 72 | painter: Painter, 73 | modifier: Modifier = Modifier, 74 | ) { 75 | Column( 76 | modifier = modifier.background(color = SlackCloneColorProvider.colors.uiBackground), 77 | horizontalAlignment = Alignment.CenterHorizontally, 78 | verticalArrangement = Arrangement.Center 79 | ) { 80 | Icon( 81 | painter = painter, 82 | contentDescription = null, 83 | tint = ChatTheme.colors.disabled, 84 | modifier = Modifier.size(96.dp), 85 | ) 86 | 87 | Spacer(Modifier.size(16.dp)) 88 | 89 | Text( 90 | text = text, 91 | style = ChatTheme.typography.title3, 92 | color = ChatTheme.colors.textLowEmphasis, 93 | textAlign = TextAlign.Center 94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /feat-channels/src/main/java/io/getstream/slackclone/uichannels/directmessages/DMessageViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichannels.directmessages 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import io.getstream.chat.android.client.ChatClient 7 | import io.getstream.chat.android.client.api.models.QueryChannelsRequest 8 | import io.getstream.chat.android.models.Channel 9 | import io.getstream.chat.android.models.Filters 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class DMessageViewModel @Inject constructor( 16 | private val chatClient: ChatClient, 17 | ) : ViewModel() { 18 | 19 | val channels = MutableStateFlow>(emptyList()) 20 | 21 | fun refresh() { 22 | viewModelScope.launch { 23 | val currentUser = requireNotNull(chatClient.getCurrentUser()) 24 | val channelsRequest = QueryChannelsRequest( 25 | filter = Filters.and( 26 | Filters.eq("type", "messaging"), 27 | Filters.`in`("members", listOf(currentUser.id)), 28 | ), 29 | offset = 0, 30 | limit = 20, 31 | ) 32 | val result = chatClient.queryChannels(channelsRequest).await() 33 | if (result.isSuccess) { 34 | channels.value = result.getOrThrow() 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /feat-channels/src/main/java/io/getstream/slackclone/uichannels/views/SlackAllChannels.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichannels.views 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.collectAsState 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.setValue 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.hilt.navigation.compose.hiltViewModel 12 | import io.getstream.slackclone.chatcore.data.ExpandCollapseModel 13 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 14 | import io.getstream.slackclone.uichannels.SlackChannelVM 15 | 16 | @Composable 17 | fun SlackAllChannels( 18 | onItemClick: (UiLayerChannels.SlackChannel) -> Unit = {}, 19 | channelVM: SlackChannelVM = hiltViewModel(), 20 | onClickAdd: () -> Unit 21 | ) { 22 | 23 | val recent = stringResource(io.getstream.slackclone.common.R.string.channels) 24 | val channelsFlow = channelVM.channels.collectAsState() 25 | val channels by channelsFlow.value.collectAsState(initial = listOf()) 26 | 27 | LaunchedEffect(key1 = Unit) { 28 | channelVM.allChannels() 29 | } 30 | 31 | var expandCollapseModel by remember { 32 | mutableStateOf( 33 | ExpandCollapseModel( 34 | 1, recent, 35 | needsPlusButton = true, 36 | isOpen = false 37 | ) 38 | ) 39 | } 40 | SKExpandCollapseColumn(expandCollapseModel = expandCollapseModel, onItemClick = onItemClick, { 41 | expandCollapseModel = expandCollapseModel.copy(isOpen = it) 42 | }, channels, onClickAdd) 43 | } 44 | -------------------------------------------------------------------------------- /feat-channels/src/main/java/io/getstream/slackclone/uichannels/views/SlackConnections.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichannels.views 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.collectAsState 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.setValue 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.hilt.navigation.compose.hiltViewModel 12 | import io.getstream.slackclone.chatcore.data.ExpandCollapseModel 13 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 14 | import io.getstream.slackclone.uichannels.SlackChannelVM 15 | 16 | @Composable 17 | fun SlackConnections( 18 | onItemClick: (UiLayerChannels.SlackChannel) -> Unit = {}, 19 | channelVM: SlackChannelVM = hiltViewModel(), 20 | onClickAdd: () -> Unit 21 | 22 | ) { 23 | val recent = stringResource(io.getstream.slackclone.common.R.string.connections) 24 | val channelsFlow = channelVM.channels.collectAsState() 25 | val channels by channelsFlow.value.collectAsState(initial = listOf()) 26 | 27 | LaunchedEffect(key1 = Unit) { 28 | channelVM.allChannels() 29 | } 30 | 31 | var expandCollapseModel by remember { 32 | mutableStateOf( 33 | ExpandCollapseModel( 34 | 1, recent, 35 | needsPlusButton = false, 36 | isOpen = false 37 | ) 38 | ) 39 | } 40 | SKExpandCollapseColumn(expandCollapseModel, onItemClick, { 41 | expandCollapseModel = expandCollapseModel.copy(isOpen = it) 42 | }, channels, onClickAdd) 43 | } 44 | -------------------------------------------------------------------------------- /feat-channels/src/main/java/io/getstream/slackclone/uichannels/views/SlackDirectMessages.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichannels.views 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.collectAsState 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.setValue 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.hilt.navigation.compose.hiltViewModel 12 | import io.getstream.slackclone.chatcore.data.ExpandCollapseModel 13 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 14 | import io.getstream.slackclone.uichannels.SlackChannelVM 15 | 16 | @Composable 17 | fun SlackDirectMessages( 18 | onItemClick: (UiLayerChannels.SlackChannel) -> Unit = {}, 19 | channelVM: SlackChannelVM = hiltViewModel(), 20 | onClickAdd: () -> Unit 21 | ) { 22 | val recent = stringResource(io.getstream.slackclone.common.R.string.direct_messages) 23 | val channelsFlow = channelVM.channels.collectAsState() 24 | val channels by channelsFlow.value.collectAsState(initial = listOf()) 25 | 26 | LaunchedEffect(key1 = Unit) { 27 | channelVM.loadDirectMessageChannels() 28 | } 29 | var expandCollapseModel by remember { 30 | mutableStateOf( 31 | ExpandCollapseModel( 32 | 1, recent, 33 | needsPlusButton = true, 34 | isOpen = false 35 | ) 36 | ) 37 | } 38 | SKExpandCollapseColumn(expandCollapseModel, onItemClick, { 39 | expandCollapseModel = expandCollapseModel.copy(isOpen = it) 40 | }, channels, onClickAdd) 41 | } 42 | -------------------------------------------------------------------------------- /feat-channels/src/main/java/io/getstream/slackclone/uichannels/views/SlackRecentChannels.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichannels.views 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.collectAsState 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.setValue 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.hilt.navigation.compose.hiltViewModel 12 | import io.getstream.slackclone.chatcore.data.ExpandCollapseModel 13 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 14 | import io.getstream.slackclone.iochannels.R 15 | import io.getstream.slackclone.uichannels.SlackChannelVM 16 | 17 | @Composable 18 | fun SlackRecentChannels( 19 | onItemClick: (UiLayerChannels.SlackChannel) -> Unit = {}, 20 | channelVM: SlackChannelVM = hiltViewModel(), 21 | onClickAdd: () -> Unit 22 | ) { 23 | val recent = stringResource(io.getstream.slackclone.common.R.string.Recent) 24 | val channelsFlow = channelVM.channels.collectAsState() 25 | val channels by channelsFlow.value.collectAsState(initial = listOf()) 26 | 27 | LaunchedEffect(key1 = Unit) { 28 | channelVM.allChannels() 29 | } 30 | 31 | var expandCollapseModel by remember { 32 | mutableStateOf( 33 | ExpandCollapseModel( 34 | 1, recent, 35 | needsPlusButton = false, 36 | isOpen = true 37 | ) 38 | ) 39 | } 40 | SKExpandCollapseColumn(expandCollapseModel, onItemClick, { 41 | expandCollapseModel = expandCollapseModel.copy(isOpen = it) 42 | }, channels, onClickAdd) 43 | } 44 | -------------------------------------------------------------------------------- /feat-channels/src/main/java/io/getstream/slackclone/uichannels/views/SlackStarredChannels.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichannels.views 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.collectAsState 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.setValue 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.hilt.navigation.compose.hiltViewModel 12 | import io.getstream.slackclone.chatcore.data.ExpandCollapseModel 13 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 14 | import io.getstream.slackclone.uichannels.SlackChannelVM 15 | 16 | @Composable 17 | fun SlackStarredChannels( 18 | onItemClick: (UiLayerChannels.SlackChannel) -> Unit = {}, 19 | channelVM: SlackChannelVM = hiltViewModel(), 20 | onClickAdd: () -> Unit 21 | ) { 22 | val recent = stringResource(io.getstream.slackclone.common.R.string.starred) 23 | val channelsFlow = channelVM.channels.collectAsState() 24 | val channels by channelsFlow.value.collectAsState(initial = listOf()) 25 | 26 | LaunchedEffect(key1 = Unit) { 27 | channelVM.allChannels() 28 | channelVM.loadStarredChannels() 29 | } 30 | 31 | var expandCollapseModel by remember { 32 | mutableStateOf( 33 | ExpandCollapseModel( 34 | 1, recent, 35 | needsPlusButton = false, 36 | isOpen = false 37 | ) 38 | ) 39 | } 40 | SKExpandCollapseColumn(expandCollapseModel, onItemClick, { 41 | expandCollapseModel = expandCollapseModel.copy(isOpen = it) 42 | }, channels, onClickAdd) 43 | } 44 | -------------------------------------------------------------------------------- /feat-channels/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Create 4 | Name 5 | Make Private 6 | When a channel is set to private, it can only be viewed or joined by invitation. 7 | Share outside organization 8 | Share this channel with people from other companies or teams, and work together right in Slack. 9 | This can\'t be undone. A private channel cannot be made public later on. 10 | -------------------------------------------------------------------------------- /feat-chat/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feat-chat/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_KSP) 5 | id(BuildPlugins.DAGGER_HILT) 6 | id(BuildPlugins.COMPOSE_COMPILER) 7 | id(BuildPlugins.KOTLIN_PARCELABLE_PLUGIN) 8 | } 9 | 10 | android { 11 | compileSdk = ProjectProperties.COMPILE_SDK 12 | namespace = "io.getstream.slackclone.uichat" 13 | 14 | defaultConfig { 15 | minSdk = (ProjectProperties.MIN_SDK) 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | getByName("release") { 21 | isMinifyEnabled = false 22 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 23 | } 24 | } 25 | 26 | buildFeatures { 27 | compose = true 28 | } 29 | 30 | packaging { 31 | resources.excludes.add("META-INF/LICENSE.txt") 32 | resources.excludes.add("META-INF/NOTICE.txt") 33 | resources.excludes.add("LICENSE.txt") 34 | resources.excludes.add("/META-INF/{AL2.0,LGPL2.1}") 35 | } 36 | 37 | compileOptions { 38 | sourceCompatibility = JavaVersion.VERSION_1_8 39 | targetCompatibility = JavaVersion.VERSION_1_8 40 | } 41 | 42 | kotlinOptions { 43 | jvmTarget = "1.8" 44 | } 45 | 46 | } 47 | 48 | dependencies { 49 | /*Kotlin*/ 50 | implementation(project(":data")) 51 | implementation(project(":domain")) 52 | implementation(project(":common")) 53 | implementation(project(":navigator")) 54 | implementation(project(":commonui")) 55 | implementation(project(":feat-chatcore")) 56 | 57 | api(Lib.Android.COMPOSE_UI) 58 | api(Lib.Android.COMPOSE_MATERIAL) 59 | api(Lib.Android.COMPOSE_FOUNDATION) 60 | implementation(Lib.Android.ACCOMPANIST_SYSTEM_UI_CONTROLLER) 61 | api(Lib.Android.COMPOSE_UI) 62 | api(Lib.Android.COMPOSE_TOOLING) 63 | implementation(Lib.Android.LANDSCAPIST_GLIDE) 64 | debugApi(Lib.Android.COMPOSE_DEBUG_TOOLING) 65 | api(Lib.Android.ACTIVITY_COMPOSE) 66 | api(Lib.Android.CONSTRAINT_LAYOUT_COMPOSE) 67 | implementation(Lib.Paging.PAGING_3) 68 | implementation(Lib.Paging.PAGING_COMPOSE) 69 | 70 | api(Lib.Android.APP_COMPAT) 71 | api(Lib.Kotlin.KTX_CORE) 72 | 73 | /*DI*/ 74 | api(Lib.Di.hiltAndroid) 75 | api(Lib.Di.hiltNavigationCompose) 76 | 77 | ksp(Lib.Di.hiltCompiler) 78 | ksp(Lib.Di.hiltAndroidCompiler) 79 | 80 | /* Logger */ 81 | api(Lib.Logger.TIMBER) 82 | /* Async */ 83 | api(Lib.Async.COROUTINES) 84 | api(Lib.Async.COROUTINES_ANDROID) 85 | 86 | testImplementation(TestLib.JUNIT) 87 | testImplementation(TestLib.CORE_TEST) 88 | testImplementation(TestLib.ANDROID_JUNIT) 89 | testImplementation(TestLib.ARCH_CORE) 90 | testImplementation(TestLib.MOCK_WEB_SERVER) 91 | testImplementation(TestLib.ROBO_ELECTRIC) 92 | testImplementation(TestLib.COROUTINES) 93 | testImplementation(TestLib.MOCKK) 94 | testImplementation(TestLib.TURBINE) 95 | } -------------------------------------------------------------------------------- /feat-chat/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /feat-chat/src/main/java/io/getstream/slackclone/uichat/chatthread/ChatScreenUI.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichat.chatthread 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.layout.statusBarsPadding 9 | import androidx.compose.material.Icon 10 | import androidx.compose.material.IconButton 11 | import androidx.compose.material.Scaffold 12 | import androidx.compose.material.icons.Icons 13 | import androidx.compose.material.icons.filled.ArrowBack 14 | import androidx.compose.material.icons.filled.Call 15 | import androidx.compose.material.rememberScaffoldState 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.unit.dp 20 | import io.getstream.chat.android.models.Channel 21 | import io.getstream.slackclone.chatcore.views.SlackChannelItem 22 | import io.getstream.slackclone.commonui.material.SlackSurfaceAppBar 23 | import io.getstream.slackclone.commonui.theme.SlackCloneColorProvider 24 | import io.getstream.slackclone.commonui.theme.SlackCloneTheme 25 | import io.getstream.slackclone.uichat.chatthread.composables.ChatScreenContent 26 | 27 | @OptIn(androidx.constraintlayout.compose.ExperimentalMotionApi::class) 28 | @Composable 29 | fun ChatScreenUI( 30 | modifier: Modifier, 31 | slackChannel: Channel, 32 | onBackClick: () -> Unit, 33 | ) { 34 | val scaffoldState = rememberScaffoldState() 35 | 36 | SlackCloneTheme { 37 | Scaffold( 38 | backgroundColor = SlackCloneColorProvider.colors.uiBackground, 39 | contentColor = SlackCloneColorProvider.colors.textSecondary, 40 | modifier = modifier 41 | .statusBarsPadding(), 42 | scaffoldState = scaffoldState, 43 | snackbarHost = { 44 | scaffoldState.snackbarHostState 45 | }, 46 | topBar = { 47 | ChatAppBar(onBackClick, slackChannel) 48 | } 49 | ) { innerPadding -> 50 | Box( 51 | modifier = Modifier 52 | .padding(innerPadding) 53 | ) { 54 | ChatScreenContent(slackChannel = slackChannel) 55 | } 56 | } 57 | } 58 | } 59 | 60 | @Composable 61 | private fun ChatAppBar(onBackClick: () -> Unit, slackChannel: Channel) { 62 | SlackSurfaceAppBar(backgroundColor = SlackCloneColorProvider.colors.appBarColor) { 63 | IconButton(onClick = { onBackClick() }) { 64 | Icon( 65 | imageVector = Icons.Default.ArrowBack, 66 | contentDescription = null, 67 | tint = SlackCloneColorProvider.colors.appBarIconColor, 68 | modifier = Modifier.size(24.dp) 69 | ) 70 | } 71 | Column( 72 | Modifier.weight(1f), 73 | verticalArrangement = Arrangement.Center, 74 | horizontalAlignment = Alignment.CenterHorizontally 75 | ) { 76 | SlackChannelItem( 77 | slackChannel = slackChannel, 78 | textColor = SlackCloneColorProvider.colors.appBarTextTitleColor 79 | ) {} 80 | } 81 | IconButton(onClick = { }) { 82 | Icon( 83 | imageVector = Icons.Default.Call, 84 | contentDescription = null, 85 | tint = SlackCloneColorProvider.colors.appBarIconColor, 86 | modifier = Modifier 87 | .size(24.dp) 88 | ) 89 | } 90 | } 91 | } 92 | 93 | enum class BoxState { Collapsed, Expanded } 94 | -------------------------------------------------------------------------------- /feat-chat/src/main/java/io/getstream/slackclone/uichat/chatthread/ChatScreenVM.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichat.chatthread 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 7 | import io.getstream.slackclone.domain.mappers.UiModelMapper 8 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 9 | import io.getstream.slackclone.domain.model.message.ChannelMessage 10 | import io.getstream.slackclone.domain.usecases.channels.UseCaseCreateRemoteChannel 11 | import io.getstream.slackclone.domain.usecases.channels.UseCaseSendMessageToChannel 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class ChatScreenVM @Inject constructor( 18 | private val useCaseRemoteChannel: UseCaseCreateRemoteChannel, 19 | private val useCaseSendMessageToChannel: UseCaseSendMessageToChannel, 20 | private val channelMapper: UiModelMapper 21 | ) : ViewModel() { 22 | val message = MutableStateFlow("") 23 | val chatBoxState = MutableStateFlow(BoxState.Collapsed) 24 | 25 | fun createChannel(uiLayerChannels: UiLayerChannels.SlackChannel) { 26 | viewModelScope.launch { 27 | useCaseRemoteChannel.perform( 28 | channelMapper.mapToDomain(uiLayerChannels) 29 | ) 30 | } 31 | } 32 | 33 | fun sendMessage(cid: String, message: String) { 34 | viewModelScope.launch { 35 | useCaseSendMessageToChannel.perform(ChannelMessage(cid, message)) 36 | 37 | // clear chat box & input states 38 | chatBoxState.value = BoxState.Collapsed 39 | this@ChatScreenVM.message.value = "" 40 | } 41 | } 42 | 43 | fun switchChatBoxState() { 44 | chatBoxState.value = 45 | if (chatBoxState.value == BoxState.Collapsed) BoxState.Expanded else BoxState.Collapsed 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /feat-chat/src/main/java/io/getstream/slackclone/uichat/chatthread/composables/ChatMessageDateSeparator.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichat.chatthread.composables 2 | 3 | import android.text.format.DateUtils 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material.Surface 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import io.getstream.chat.android.compose.ui.theme.ChatTheme 15 | import io.getstream.chat.android.ui.common.state.messages.list.DateSeparatorItemState 16 | 17 | @Composable 18 | fun ChatMessageDateSeparator(dateSeparator: DateSeparatorItemState) { 19 | Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { 20 | Surface( 21 | modifier = Modifier 22 | .padding(vertical = 8.dp), 23 | color = ChatTheme.colors.overlayDark, 24 | shape = RoundedCornerShape(16.dp) 25 | ) { 26 | Text( 27 | modifier = Modifier.padding(vertical = 2.dp, horizontal = 16.dp), 28 | text = DateUtils.getRelativeTimeSpanString( 29 | dateSeparator.date.time, 30 | System.currentTimeMillis(), 31 | DateUtils.DAY_IN_MILLIS, 32 | DateUtils.FORMAT_ABBREV_RELATIVE 33 | ).toString(), 34 | color = ChatTheme.colors.barsBackground, 35 | style = ChatTheme.typography.body 36 | ) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /feat-chat/src/main/java/io/getstream/slackclone/uichat/chatthread/composables/ChatMessagesUI.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichat.chatthread.composables 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 9 | import io.getstream.chat.android.compose.ui.components.LoadingIndicator 10 | import io.getstream.chat.android.compose.ui.messages.list.Messages 11 | import io.getstream.chat.android.compose.ui.theme.ChatTheme 12 | import io.getstream.chat.android.compose.ui.util.rememberMessageListState 13 | import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel 14 | import io.getstream.chat.android.models.MessagesState 15 | import io.getstream.chat.android.ui.common.state.messages.list.DateSeparatorItemState 16 | import io.getstream.chat.android.ui.common.state.messages.list.MessageItemState 17 | import io.getstream.chat.android.ui.common.state.messages.list.MessageListState 18 | import io.getstream.chat.android.ui.common.state.messages.list.SelectedMessageState 19 | import io.getstream.chat.android.ui.common.state.messages.list.SystemMessageItemState 20 | import io.getstream.slackclone.uichat.chatthread.composables.reactions.SlackCloneReactionFactory 21 | 22 | @Composable 23 | fun ChatMessagesUI( 24 | modifier: Modifier, 25 | listViewModel: MessageListViewModel, 26 | currentState: MessageListState, 27 | selectedState: SelectedMessageState?, 28 | ) { 29 | val keyboardController = LocalSoftwareKeyboardController.current 30 | val isLoading = currentState.isLoading 31 | 32 | ChatTheme( 33 | reactionIconFactory = SlackCloneReactionFactory() 34 | ) { 35 | Box(modifier = modifier.fillMaxSize()) { 36 | when { 37 | isLoading -> LoadingIndicator(Modifier.fillMaxSize()) 38 | !isLoading && currentState.messageItems.isNotEmpty() -> 39 | Messages( 40 | modifier = Modifier 41 | .fillMaxSize() 42 | .background(ChatTheme.colors.appBackground), 43 | messagesState = listViewModel.currentMessagesState, 44 | messagesLazyListState = rememberMessageListState(parentMessageId = listViewModel.currentMessagesState.parentMessageId), 45 | itemContent = { messageListItemState -> 46 | when (messageListItemState) { 47 | is MessageItemState -> ChatMessage( 48 | messageItemState = messageListItemState, 49 | onLongItemClick = { 50 | listViewModel.selectMessage(it) 51 | keyboardController?.hide() 52 | }, 53 | onReactionsClick = { 54 | listViewModel.selectReactions(it) 55 | } 56 | ) 57 | 58 | is DateSeparatorItemState -> ChatMessageDateSeparator(dateSeparator = messageListItemState) 59 | is SystemMessageItemState -> ChatSystemMessage(systemMessageState = messageListItemState) 60 | else -> Unit 61 | } 62 | }, 63 | onMessagesStartReached = {}, 64 | onLastVisibleMessageChanged = {}, 65 | onScrolledToBottom = {}, 66 | onMessagesEndReached = {}, 67 | onScrollToBottom = {} 68 | ) 69 | 70 | else -> MessageListEmptyContent(Modifier.fillMaxSize()) 71 | } 72 | 73 | ChatMessageReactionSelectMenu( 74 | selectedMessageState = selectedState, 75 | listViewModel = listViewModel 76 | ) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /feat-chat/src/main/java/io/getstream/slackclone/uichat/chatthread/composables/ChatSystemMessage.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichat.chatthread.composables 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.text.style.TextAlign 9 | import androidx.compose.ui.unit.dp 10 | import io.getstream.chat.android.compose.ui.theme.ChatTheme 11 | import io.getstream.chat.android.ui.common.state.messages.list.SystemMessageItemState 12 | 13 | @Composable 14 | fun ChatSystemMessage(systemMessageState: SystemMessageItemState) { 15 | Text( 16 | modifier = Modifier 17 | .fillMaxWidth() 18 | .padding(vertical = 12.dp, horizontal = 16.dp), 19 | text = systemMessageState.message.text, 20 | color = ChatTheme.colors.textLowEmphasis, 21 | style = ChatTheme.typography.footnoteBold, 22 | textAlign = TextAlign.Center 23 | ) 24 | } -------------------------------------------------------------------------------- /feat-chat/src/main/java/io/getstream/slackclone/uichat/chatthread/composables/MessageListEmptyContent.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichat.chatthread.composables 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.stringResource 10 | import androidx.compose.ui.text.style.TextAlign 11 | import io.getstream.chat.android.compose.R 12 | import io.getstream.chat.android.compose.ui.theme.ChatTheme 13 | 14 | @Composable 15 | fun MessageListEmptyContent(modifier: Modifier) { 16 | Box( 17 | modifier = modifier.background(color = ChatTheme.colors.appBackground), 18 | contentAlignment = Alignment.Center 19 | ) { 20 | Text( 21 | text = stringResource(R.string.stream_compose_message_list_empty_messages), 22 | style = ChatTheme.typography.body, 23 | color = ChatTheme.colors.textLowEmphasis, 24 | textAlign = TextAlign.Center 25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /feat-chat/src/main/java/io/getstream/slackclone/uichat/chatthread/composables/reactions/ChatMessageReactionOptionItem.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichat.chatthread.composables.reactions 2 | 3 | import androidx.compose.animation.core.animateDpAsState 4 | import androidx.compose.animation.core.keyframes 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.interaction.MutableInteractionSource 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.offset 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.material.ripple 12 | import androidx.compose.material.ripple.rememberRipple 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.rotate 20 | import androidx.compose.ui.draw.scale 21 | import androidx.compose.ui.unit.dp 22 | import io.getstream.chat.android.compose.state.reactionoptions.ReactionOptionItemState 23 | 24 | @Composable 25 | fun CustomReactionOptionItem( 26 | option: ReactionOptionItemState, 27 | springValue: Float, 28 | onReactionOptionSelected: (ReactionOptionItemState) -> Unit 29 | ) { 30 | var currentState by remember { mutableStateOf(ReactionButtonState.IDLE) } 31 | val normalIconSize = 24.dp 32 | val animatedIconSize = 50.dp 33 | val sizeAnimation by animateDpAsState( 34 | if (currentState == ReactionButtonState.ACTIVE) 24.1.dp else 24.dp, 35 | animationSpec = keyframes { 36 | durationMillis = 200 37 | animatedIconSize.at(100) 38 | normalIconSize.at(200) 39 | }, 40 | finishedListener = { 41 | onReactionOptionSelected(option) 42 | } 43 | ) 44 | 45 | Row { 46 | Image( 47 | modifier = Modifier 48 | .size(size = sizeAnimation) 49 | .scale(springValue) 50 | .offset(x = (-15).dp + (15 * springValue).dp) 51 | .rotate(-45f + (45 * springValue)) 52 | .clickable( 53 | interactionSource = remember { MutableInteractionSource() }, 54 | indication = ripple(bounded = false), 55 | onClick = { 56 | currentState = if (currentState == ReactionButtonState.IDLE) 57 | ReactionButtonState.ACTIVE else ReactionButtonState.IDLE 58 | onReactionOptionSelected(option) 59 | } 60 | ), 61 | painter = option.painter, 62 | contentDescription = option.type, 63 | ) 64 | } 65 | } 66 | 67 | enum class ReactionButtonState { 68 | IDLE, ACTIVE 69 | } -------------------------------------------------------------------------------- /feat-chat/src/main/java/io/getstream/slackclone/uichat/chatthread/composables/reactions/SlackCloneReactionFactory.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichat.chatthread.composables.reactions 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.res.painterResource 5 | import io.getstream.chat.android.compose.ui.util.ReactionDrawable 6 | import io.getstream.chat.android.compose.ui.util.ReactionIcon 7 | import io.getstream.chat.android.compose.ui.util.ReactionIconFactory 8 | import io.getstream.slackclone.uichat.R 9 | 10 | class SlackCloneReactionFactory( 11 | private val supportedReactions: Map = mapOf( 12 | "good" to ReactionDrawable( 13 | iconResId = R.drawable.thumbsup, 14 | selectedIconResId = R.drawable.thumbsup 15 | ), 16 | "love" to ReactionDrawable( 17 | iconResId = R.drawable.love, 18 | selectedIconResId = R.drawable.love 19 | ), 20 | "smile" to ReactionDrawable( 21 | iconResId = R.drawable.smile, 22 | selectedIconResId = R.drawable.smile 23 | ), 24 | "joy" to ReactionDrawable( 25 | iconResId = R.drawable.joy, 26 | selectedIconResId = R.drawable.joy 27 | ), 28 | "wink" to ReactionDrawable( 29 | iconResId = R.drawable.wink, 30 | selectedIconResId = R.drawable.wink 31 | ) 32 | ) 33 | ) : ReactionIconFactory { 34 | 35 | @Composable 36 | override fun createReactionIcon(type: String): ReactionIcon { 37 | val reactionDrawable = requireNotNull(supportedReactions[type]) 38 | return ReactionIcon( 39 | painter = painterResource(reactionDrawable.iconResId), 40 | selectedPainter = painterResource(reactionDrawable.selectedIconResId) 41 | ) 42 | } 43 | 44 | @Composable 45 | override fun createReactionIcons(): Map { 46 | return supportedReactions.mapValues { 47 | createReactionIcon(it.key) 48 | } 49 | } 50 | 51 | override fun isReactionSupported(type: String): Boolean { 52 | return supportedReactions.containsKey(type) 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /feat-chat/src/main/java/io/getstream/slackclone/uichat/chatthread/composables/reactions/SlackMessageReactionItem.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichat.chatthread.composables.reactions 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.layout.wrapContentSize 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.unit.sp 16 | import io.getstream.chat.android.compose.state.reactionoptions.ReactionOptionItemState 17 | import io.getstream.slackclone.commonui.theme.SlackCloneColorProvider 18 | 19 | @Composable 20 | fun SlackMessageReactionItem( 21 | option: ReactionOptionItemState, 22 | score: Int, 23 | modifier: Modifier = Modifier, 24 | ) { 25 | Row( 26 | modifier 27 | .wrapContentSize() 28 | .padding(horizontal = 4.dp), 29 | verticalAlignment = Alignment.CenterVertically 30 | ) { 31 | Image( 32 | modifier = Modifier 33 | .size(20.dp) 34 | .padding(2.dp) 35 | .align(Alignment.CenterVertically), 36 | painter = option.painter, 37 | contentDescription = null 38 | ) 39 | 40 | Spacer(modifier = Modifier.width(4.dp)) 41 | 42 | Text( 43 | text = score.toString(), 44 | color = SlackCloneColorProvider.colors.textPrimary, 45 | fontSize = 11.sp 46 | ) 47 | } 48 | } -------------------------------------------------------------------------------- /feat-chat/src/main/java/io/getstream/slackclone/uichat/newchat/NewChatThreadVM.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uichat.newchat 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.paging.map 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 7 | import io.getstream.slackclone.domain.mappers.UiModelMapper 8 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 9 | import io.getstream.slackclone.domain.usecases.channels.UseCaseSearchChannel 10 | import io.getstream.slackclone.navigator.ComposeNavigator 11 | import io.getstream.slackclone.navigator.NavigationKeys 12 | import io.getstream.slackclone.navigator.SlackScreen 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.map 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class NewChatThreadVM @Inject constructor( 19 | private val composeNavigator: ComposeNavigator, 20 | private val ucFetchChannels: UseCaseSearchChannel, 21 | private val chatPresentationMapper: UiModelMapper 22 | ) : 23 | ViewModel() { 24 | 25 | val search = MutableStateFlow("") 26 | var users = MutableStateFlow(flow("")) 27 | 28 | private fun flow(search: String) = ucFetchChannels.performStreaming(search).map { channels -> 29 | channels.map { channel -> 30 | chatPresentationMapper.mapToPresentation(channel) 31 | } 32 | } 33 | 34 | fun search(newValue: String) { 35 | search.value = newValue 36 | users.value = flow(newValue) 37 | } 38 | 39 | fun navigate(channel: UiLayerChannels.SlackChannel) { 40 | composeNavigator.navigateBackWithResult( 41 | NavigationKeys.navigateChannel, 42 | channel.uuid!!, 43 | SlackScreen.Dashboard.name 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /feat-chat/src/main/res/drawable/joy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/feat-chat/src/main/res/drawable/joy.png -------------------------------------------------------------------------------- /feat-chat/src/main/res/drawable/love.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/feat-chat/src/main/res/drawable/love.png -------------------------------------------------------------------------------- /feat-chat/src/main/res/drawable/smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/feat-chat/src/main/res/drawable/smile.png -------------------------------------------------------------------------------- /feat-chat/src/main/res/drawable/thumbsup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/feat-chat/src/main/res/drawable/thumbsup.png -------------------------------------------------------------------------------- /feat-chat/src/main/res/drawable/wink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/feat-chat/src/main/res/drawable/wink.png -------------------------------------------------------------------------------- /feat-chat/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Search for a channel or conversation 4 | New Message 5 | Clear 6 | -------------------------------------------------------------------------------- /feat-chatcore/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feat-chatcore/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_KSP) 5 | id(BuildPlugins.DAGGER_HILT) 6 | id(BuildPlugins.COMPOSE_COMPILER) 7 | id(BuildPlugins.KOTLIN_PARCELABLE_PLUGIN) 8 | } 9 | 10 | android { 11 | compileSdk = ProjectProperties.COMPILE_SDK 12 | namespace = "io.getstream.slackclone.chatcore" 13 | 14 | defaultConfig { 15 | minSdk = (ProjectProperties.MIN_SDK) 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | getByName("release") { 21 | isMinifyEnabled = false 22 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 23 | } 24 | } 25 | 26 | buildFeatures { 27 | compose = true 28 | } 29 | 30 | packaging { 31 | resources.excludes.add("META-INF/LICENSE.txt") 32 | resources.excludes.add("META-INF/NOTICE.txt") 33 | resources.excludes.add("LICENSE.txt") 34 | resources.excludes.add("/META-INF/{AL2.0,LGPL2.1}") 35 | } 36 | 37 | compileOptions { 38 | sourceCompatibility = JavaVersion.VERSION_1_8 39 | targetCompatibility = JavaVersion.VERSION_1_8 40 | } 41 | 42 | kotlinOptions { 43 | jvmTarget = "1.8" 44 | } 45 | 46 | } 47 | 48 | dependencies { 49 | /*Kotlin*/ 50 | implementation(project(":data")) 51 | implementation(project(":domain")) 52 | implementation(project(":common")) 53 | implementation(project(":commonui")) 54 | 55 | api(Lib.Android.COMPOSE_UI) 56 | api(Lib.Android.COMPOSE_MATERIAL) 57 | implementation(Lib.Android.ACCOMPANIST_SYSTEM_UI_CONTROLLER) 58 | api(Lib.Android.COMPOSE_UI) 59 | api(Lib.Android.COMPOSE_TOOLING) 60 | implementation(Lib.Android.LANDSCAPIST_GLIDE) 61 | debugApi(Lib.Android.COMPOSE_DEBUG_TOOLING) 62 | api(Lib.Android.ACTIVITY_COMPOSE) 63 | api(Lib.Android.CONSTRAINT_LAYOUT_COMPOSE) 64 | implementation(Lib.Paging.PAGING_3) 65 | implementation(Lib.Paging.PAGING_COMPOSE) 66 | implementation(Lib.Android.APP_STARTUP) 67 | 68 | api(Lib.Android.APP_COMPAT) 69 | api(Lib.Kotlin.KTX_CORE) 70 | 71 | /* Stream Chat SDK */ 72 | api(Lib.STREAM.STREAM_CHAT_COMPOSE) 73 | implementation(Lib.STREAM.STREAM_CHAT_OFFLINE) 74 | 75 | /*DI*/ 76 | api(Lib.Di.hiltAndroid) 77 | api(Lib.Di.hiltNavigationCompose) 78 | 79 | ksp(Lib.Di.hiltCompiler) 80 | ksp(Lib.Di.hiltAndroidCompiler) 81 | 82 | /* Logger */ 83 | api(Lib.Logger.TIMBER) 84 | /* Async */ 85 | api(Lib.Async.COROUTINES) 86 | api(Lib.Async.COROUTINES_ANDROID) 87 | 88 | testImplementation(TestLib.JUNIT) 89 | testImplementation(TestLib.CORE_TEST) 90 | testImplementation(TestLib.ANDROID_JUNIT) 91 | testImplementation(TestLib.ARCH_CORE) 92 | testImplementation(TestLib.MOCK_WEB_SERVER) 93 | testImplementation(TestLib.ROBO_ELECTRIC) 94 | testImplementation(TestLib.COROUTINES) 95 | testImplementation(TestLib.MOCKK) 96 | testImplementation(TestLib.TURBINE) 97 | } -------------------------------------------------------------------------------- /feat-chatcore/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /feat-chatcore/src/main/java/io/getstream/slackclone/chatcore/ChannelUIModelMapper.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.chatcore 2 | 3 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 4 | import io.getstream.slackclone.domain.mappers.UiModelMapper 5 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 6 | import javax.inject.Inject 7 | 8 | class ChannelUIModelMapper @Inject constructor() : 9 | UiModelMapper { 10 | override fun mapToPresentation(model: DomainLayerChannels.SlackChannel): UiLayerChannels.SlackChannel { 11 | return UiLayerChannels.SlackChannel( 12 | model.name, 13 | model.isPrivate, 14 | model.uuid, 15 | model.createdDate, 16 | model.modifiedDate, 17 | model.isMuted, 18 | model.isOneToOne, 19 | model.avatarUrl 20 | ) 21 | } 22 | 23 | override fun mapToDomain(modelItem: UiLayerChannels.SlackChannel): DomainLayerChannels.SlackChannel { 24 | return DomainLayerChannels.SlackChannel( 25 | modelItem.uuid, 26 | modelItem.name, 27 | modelItem.createdDate, 28 | modelItem.modifiedDate, 29 | modelItem.isMuted, 30 | modelItem.isPrivate, 31 | false, 32 | false, 33 | modelItem.isOneToOne, 34 | modelItem.pictureUrl 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /feat-chatcore/src/main/java/io/getstream/slackclone/chatcore/data/ExpandCollapseModel.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.chatcore.data 2 | 3 | data class ExpandCollapseModel( 4 | val id: Int, 5 | val title: String, 6 | val needsPlusButton: Boolean, 7 | var isOpen: Boolean 8 | ) 9 | -------------------------------------------------------------------------------- /feat-chatcore/src/main/java/io/getstream/slackclone/chatcore/data/UiLayerChannels.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.chatcore.data 2 | 3 | interface UiLayerChannels { 4 | data class SlackChannel( 5 | val name: String?, 6 | val isPrivate: Boolean?, 7 | val uuid: String?, 8 | val createdDate: Long?, 9 | val modifiedDate: Long?, 10 | val isMuted: Boolean?, 11 | val isOneToOne: Boolean?, 12 | val pictureUrl: String? 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /feat-chatcore/src/main/java/io/getstream/slackclone/chatcore/extensions/ModelExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.chatcore.extensions 2 | 3 | import io.getstream.chat.android.client.ChatClient 4 | import io.getstream.chat.android.models.Channel 5 | import io.getstream.chat.android.models.Member 6 | import io.getstream.chat.android.models.User 7 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 8 | import io.getstream.slackclone.domain.model.message.DomainLayerMessages 9 | import java.util.Date 10 | import kotlin.random.Random 11 | 12 | fun UiLayerChannels.SlackChannel.toStreamChannel(): Channel { 13 | return Channel( 14 | id = uuid ?: "", 15 | type = "messaging", 16 | name = name ?: "", 17 | image = pictureUrl ?: "", 18 | hidden = isPrivate ?: false, 19 | members = listOf( 20 | Member(User(name = name ?: "")), 21 | Member(ChatClient.instance().getCurrentUser() ?: User()) 22 | ), 23 | createdAt = Date(createdDate ?: System.currentTimeMillis()), 24 | updatedAt = Date(modifiedDate ?: System.currentTimeMillis()), 25 | memberCount = if (isOneToOne == true) 2 else Random.nextInt(100) + 2, 26 | ) 27 | } 28 | 29 | fun Channel.toSlackUIChannel(): UiLayerChannels.SlackChannel { 30 | return UiLayerChannels.SlackChannel( 31 | name = name, 32 | isPrivate = hidden, 33 | uuid = id, 34 | createdDate = createdAt?.time ?: System.currentTimeMillis(), 35 | modifiedDate = updatedAt?.time ?: System.currentTimeMillis(), 36 | isMuted = false, 37 | isOneToOne = true, 38 | pictureUrl = image 39 | ) 40 | } 41 | 42 | fun Channel.toSlackDomainChannelMessage(): DomainLayerMessages.SlackMessage { 43 | return DomainLayerMessages.SlackMessage( 44 | uuid = id, 45 | channelId = cid, 46 | message = messages.firstOrNull()?.text ?: "Direct message", 47 | userId = members.firstOrNull()?.user?.id ?: "", 48 | createdBy = members.firstOrNull()?.user?.name ?: "", 49 | createdDate = createdAt?.time ?: System.currentTimeMillis(), 50 | modifiedDate = updatedAt?.time ?: System.currentTimeMillis(), 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /feat-chatcore/src/main/java/io/getstream/slackclone/chatcore/injection/UiModelMapperModule.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.chatcore.injection 2 | 3 | import dagger.Binds 4 | import dagger.Module 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import io.getstream.slackclone.chatcore.ChannelUIModelMapper 8 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 9 | import io.getstream.slackclone.domain.mappers.UiModelMapper 10 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 11 | import io.getstream.slackclone.domain.model.users.DomainLayerUsers 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | abstract class UiModelMapperModule { 17 | 18 | @Binds 19 | @Singleton 20 | abstract fun bindSlackUserChannelMapper(userChannelUiMapper: UserChannelUiMapper): UiModelMapper 21 | 22 | @Binds 23 | @Singleton 24 | abstract fun bindChannelUIModelMapper(channelUIModelMapper: ChannelUIModelMapper): UiModelMapper 25 | } 26 | -------------------------------------------------------------------------------- /feat-chatcore/src/main/java/io/getstream/slackclone/chatcore/injection/UserChannelUiMapper.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.chatcore.injection 2 | 3 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 4 | import io.getstream.slackclone.domain.mappers.UiModelMapper 5 | import io.getstream.slackclone.domain.model.users.DomainLayerUsers 6 | import javax.inject.Inject 7 | 8 | class UserChannelUiMapper @Inject constructor() : 9 | UiModelMapper { 10 | override fun mapToPresentation(model: DomainLayerUsers.SlackUser): UiLayerChannels.SlackChannel { 11 | TODO("Not yet implemented") 12 | } 13 | 14 | override fun mapToDomain(modelItem: UiLayerChannels.SlackChannel): DomainLayerUsers.SlackUser { 15 | TODO("Not yet implemented") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /feat-chatcore/src/main/java/io/getstream/slackclone/chatcore/startup/StreamChatInitializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 io.getstream.slackclone.chatcore.startup 18 | 19 | import android.content.Context 20 | import androidx.startup.Initializer 21 | import io.getstream.chat.android.client.ChatClient 22 | import io.getstream.chat.android.client.logger.ChatLogLevel 23 | import io.getstream.chat.android.offline.plugin.factory.StreamOfflinePluginFactory 24 | import io.getstream.chat.android.state.plugin.config.StatePluginConfig 25 | import io.getstream.chat.android.state.plugin.factory.StreamStatePluginFactory 26 | 27 | /** 28 | * StreamChatInitializer initializes all Stream Client components. 29 | */ 30 | class StreamChatInitializer : Initializer { 31 | 32 | override fun create(context: Context) { 33 | // Set up the OfflinePlugin for offline storage 34 | val offlinePluginFactory = StreamOfflinePluginFactory(appContext = context) 35 | val statePluginFactory = 36 | StreamStatePluginFactory(config = StatePluginConfig(), appContext = context) 37 | 38 | // Set up the client for API calls with the plugin for offline storage 39 | ChatClient.Builder("6wj48bfwxg4h", context) 40 | .withPlugins(offlinePluginFactory, statePluginFactory) 41 | .logLevel(ChatLogLevel.DEBUG) 42 | .build() 43 | } 44 | 45 | override fun dependencies(): List>> = emptyList() 46 | } 47 | -------------------------------------------------------------------------------- /feat-onboarding/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feat-onboarding/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_KSP) 5 | id(BuildPlugins.DAGGER_HILT) 6 | id(BuildPlugins.COMPOSE_COMPILER) 7 | id(BuildPlugins.KOTLIN_PARCELABLE_PLUGIN) 8 | } 9 | 10 | android { 11 | compileSdk = ProjectProperties.COMPILE_SDK 12 | namespace = "io.getstream.slackclone.uionboarding" 13 | 14 | defaultConfig { 15 | minSdk = (ProjectProperties.MIN_SDK) 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | getByName("release") { 21 | isMinifyEnabled = false 22 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 23 | } 24 | } 25 | 26 | buildFeatures { 27 | compose = true 28 | } 29 | 30 | packaging { 31 | resources.excludes.add("META-INF/LICENSE.txt") 32 | resources.excludes.add("META-INF/NOTICE.txt") 33 | resources.excludes.add("LICENSE.txt") 34 | resources.excludes.add("/META-INF/{AL2.0,LGPL2.1}") 35 | } 36 | 37 | compileOptions { 38 | sourceCompatibility = JavaVersion.VERSION_1_8 39 | targetCompatibility = JavaVersion.VERSION_1_8 40 | } 41 | 42 | kotlinOptions { 43 | jvmTarget = "1.8" 44 | } 45 | 46 | } 47 | 48 | dependencies { 49 | /*Kotlin*/ 50 | implementation(project(":data")) 51 | implementation(project(":domain")) 52 | implementation(project(":common")) 53 | implementation(project(":navigator")) 54 | implementation(project(":commonui")) 55 | 56 | api(Lib.Android.COMPOSE_UI) 57 | api(Lib.Android.LANDSCAPIST_GLIDE) 58 | api(Lib.Android.COMPOSE_MATERIAL) 59 | implementation(Lib.Android.ACCOMPANIST_SYSTEM_UI_CONTROLLER) 60 | api(Lib.Android.COMPOSE_UI) 61 | implementation(Lib.Android.COMPOSE_TOOLING) 62 | debugImplementation(Lib.Android.COMPOSE_DEBUG_TOOLING) 63 | api(Lib.Android.ACTIVITY_COMPOSE) 64 | api(Lib.Android.CONSTRAINT_LAYOUT_COMPOSE) 65 | 66 | api(Lib.Android.APP_COMPAT) 67 | api(Lib.Kotlin.KTX_CORE) 68 | 69 | /*Stream Chat SDK*/ 70 | api(Lib.STREAM.STREAM_CHAT_CLIENT) 71 | 72 | /*DI*/ 73 | api(Lib.Di.hiltAndroid) 74 | api(Lib.Di.hiltNavigationCompose) 75 | 76 | ksp(Lib.Di.hiltCompiler) 77 | ksp(Lib.Di.hiltAndroidCompiler) 78 | 79 | /* Logger */ 80 | api(Lib.Logger.TIMBER) 81 | /* Async */ 82 | api(Lib.Async.COROUTINES) 83 | api(Lib.Async.COROUTINES_ANDROID) 84 | 85 | testImplementation(TestLib.JUNIT) 86 | testImplementation(TestLib.CORE_TEST) 87 | testImplementation(TestLib.ANDROID_JUNIT) 88 | testImplementation(TestLib.ARCH_CORE) 89 | testImplementation(TestLib.MOCK_WEB_SERVER) 90 | testImplementation(TestLib.ROBO_ELECTRIC) 91 | testImplementation(TestLib.COROUTINES) 92 | testImplementation(TestLib.MOCKK) 93 | testImplementation(TestLib.TURBINE) 94 | } -------------------------------------------------------------------------------- /feat-onboarding/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /feat-onboarding/src/main/java/io/getstream/slackclone/uionboarding/compose/EmailInputView.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uionboarding.compose 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.wrapContentWidth 9 | import androidx.compose.foundation.text.KeyboardActions 10 | import androidx.compose.foundation.text.KeyboardOptions 11 | import androidx.compose.material.Icon 12 | import androidx.compose.material.Text 13 | import androidx.compose.material.TextField 14 | import androidx.compose.material.TextFieldDefaults 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.filled.Email 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.collectAsState 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.ExperimentalComposeUiApi 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.graphics.Color 23 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.text.input.ImeAction 26 | import androidx.compose.ui.text.input.KeyboardType 27 | import androidx.compose.ui.text.style.TextAlign 28 | import androidx.compose.ui.unit.dp 29 | import io.getstream.slackclone.commonui.theme.SlackCloneColorProvider 30 | import io.getstream.slackclone.commonui.theme.SlackCloneTypography 31 | 32 | @OptIn(ExperimentalComposeUiApi::class) 33 | @Composable 34 | fun EmailInputView( 35 | modifier: Modifier = Modifier, 36 | onBoardingVM: OnBoardingVM 37 | ) { 38 | Column( 39 | modifier = modifier 40 | .fillMaxWidth() 41 | .wrapContentWidth() 42 | ) { 43 | Text( 44 | text = "Email", 45 | style = SlackCloneTypography.caption.copy( 46 | color = SlackCloneColorProvider.colors.textPrimary.copy(alpha = 0.7f), 47 | fontWeight = FontWeight.Normal, 48 | textAlign = TextAlign.Start 49 | ), 50 | modifier = Modifier.padding(bottom = 4.dp) 51 | ) 52 | Row( 53 | modifier = modifier 54 | .fillMaxWidth(), 55 | verticalAlignment = Alignment.CenterVertically, 56 | horizontalArrangement = Arrangement.Start 57 | ) { 58 | EmailTF(onBoardingVM) 59 | } 60 | } 61 | } 62 | 63 | @ExperimentalComposeUiApi 64 | @Composable 65 | private fun EmailTF(onBoardingVM: OnBoardingVM) { 66 | val email = onBoardingVM.input.collectAsState() 67 | val keyboardController = LocalSoftwareKeyboardController.current 68 | 69 | TextField( 70 | value = email.value, 71 | onValueChange = { newEmail -> 72 | onBoardingVM.input.value = newEmail 73 | }, 74 | textStyle = textStyleField(), 75 | leadingIcon = { 76 | Icon( 77 | imageVector = Icons.Default.Email, 78 | contentDescription = null, 79 | tint = SlackCloneColorProvider.colors.textPrimary 80 | ) 81 | }, 82 | placeholder = { 83 | Text( 84 | text = "Your email address", 85 | style = textStyleField(), 86 | textAlign = TextAlign.Start 87 | ) 88 | }, 89 | keyboardOptions = KeyboardOptions.Default.copy( 90 | autoCorrectEnabled = false, 91 | keyboardType = KeyboardType.Email, 92 | imeAction = ImeAction.Done, 93 | ), 94 | keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }), 95 | colors = textFieldColors(), 96 | singleLine = true, 97 | maxLines = 1 98 | ) 99 | } 100 | 101 | @Composable 102 | private fun textFieldColors() = TextFieldDefaults.textFieldColors( 103 | backgroundColor = Color.Transparent, 104 | cursorColor = SlackCloneColorProvider.colors.textPrimary, 105 | unfocusedIndicatorColor = Color.Transparent, 106 | focusedIndicatorColor = Color.Transparent 107 | ) 108 | 109 | @Composable 110 | private fun textStyleField() = SlackCloneTypography.h6.copy( 111 | color = SlackCloneColorProvider.colors.textPrimary, 112 | fontWeight = FontWeight.Normal, 113 | textAlign = TextAlign.Start 114 | ) 115 | -------------------------------------------------------------------------------- /feat-onboarding/src/main/java/io/getstream/slackclone/uionboarding/compose/OnBoardingVM.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uionboarding.compose 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import io.getstream.slackclone.domain.model.login.LoginState 7 | import io.getstream.slackclone.domain.usecases.users.UseCaseLoginUser 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.launch 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class OnBoardingVM @Inject constructor( 14 | private val useCaseLoginUser: UseCaseLoginUser 15 | ) : ViewModel() { 16 | 17 | val input: MutableStateFlow = MutableStateFlow("") 18 | val loginState: MutableStateFlow = MutableStateFlow(LoginState.Nothing) 19 | 20 | fun connectUser() { 21 | viewModelScope.launch { 22 | loginState.value = LoginState.Loading 23 | loginState.value = useCaseLoginUser.perform(input.value) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /feat-onboarding/src/main/java/io/getstream/slackclone/uionboarding/compose/ScreenInputUI.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uionboarding.compose 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.res.stringResource 5 | import io.getstream.slackclone.commonui.theme.SlackCloneTheme 6 | import io.getstream.slackclone.navigator.ComposeNavigator 7 | import io.getstream.slackclone.uionboarding.R 8 | 9 | @Composable 10 | fun EmailAddressInputUI( 11 | composeNavigator: ComposeNavigator, 12 | onBoardingVM: OnBoardingVM 13 | ) { 14 | SlackCloneTheme { 15 | CommonInputUI( 16 | composeNavigator = composeNavigator, 17 | topView = { modifier -> EmailInputView(modifier, onBoardingVM) }, 18 | subtitleText = stringResource(id = R.string.subtitle_this_email_slack), 19 | onBoardingVM = onBoardingVM 20 | ) 21 | } 22 | } 23 | 24 | @Composable 25 | fun WorkspaceInputUI( 26 | composeNavigator: ComposeNavigator, 27 | onBoardingVM: OnBoardingVM 28 | ) { 29 | SlackCloneTheme { 30 | CommonInputUI( 31 | composeNavigator = composeNavigator, 32 | topView = { modifier -> WorkspaceInputView(modifier, onBoardingVM) }, 33 | subtitleText = stringResource(id = R.string.subtitle_this_address_slack), 34 | onBoardingVM = onBoardingVM 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /feat-onboarding/src/main/java/io/getstream/slackclone/uionboarding/compose/WorkspaceInputView.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uionboarding.compose 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.IntrinsicSize 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.width 11 | import androidx.compose.foundation.layout.wrapContentWidth 12 | import androidx.compose.foundation.text.BasicTextField 13 | import androidx.compose.material.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.collectAsState 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.SolidColor 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.text.style.TextAlign 22 | import androidx.compose.ui.text.style.TextOverflow 23 | import androidx.compose.ui.unit.dp 24 | import io.getstream.slackclone.commonui.theme.SlackCloneColorProvider 25 | import io.getstream.slackclone.commonui.theme.SlackCloneTypography 26 | 27 | @Composable 28 | fun WorkspaceInputView( 29 | modifier: Modifier, 30 | onBoardingVM: OnBoardingVM 31 | ) { 32 | Column( 33 | modifier = modifier 34 | .fillMaxWidth() 35 | .wrapContentWidth() 36 | ) { 37 | Text( 38 | text = "Workspace URL", 39 | style = SlackCloneTypography.caption.copy( 40 | color = SlackCloneColorProvider.colors.textPrimary.copy(alpha = 0.7f), 41 | fontWeight = FontWeight.Normal, 42 | textAlign = TextAlign.Start 43 | ), 44 | modifier = Modifier.padding(bottom = 4.dp) 45 | ) 46 | Row( 47 | modifier = modifier 48 | .fillMaxWidth(), 49 | verticalAlignment = Alignment.CenterVertically, 50 | horizontalArrangement = Arrangement.Start 51 | ) { 52 | TextHttps() 53 | WorkspaceTF(onBoardingVM) 54 | TextSlackCom() 55 | } 56 | } 57 | } 58 | 59 | @Composable 60 | private fun TextHttps() { 61 | Text( 62 | text = "https://", 63 | style = textStyleField().copy( 64 | color = SlackCloneColorProvider.colors.textPrimary.copy( 65 | alpha = 0.4f 66 | ) 67 | ) 68 | ) 69 | } 70 | 71 | @Composable 72 | private fun TextSlackCom() { 73 | Text( 74 | ".slack.com", 75 | style = textStyleField().copy( 76 | color = SlackCloneColorProvider.colors.textPrimary.copy( 77 | alpha = 0.4f 78 | ) 79 | ), 80 | overflow = TextOverflow.Clip, 81 | maxLines = 1 82 | ) 83 | } 84 | 85 | @Composable 86 | private fun WorkspaceTF(onBoardingVM: OnBoardingVM) { 87 | val workspace by onBoardingVM.input.collectAsState() 88 | 89 | BasicTextField( 90 | value = workspace, 91 | onValueChange = { newEmail -> onBoardingVM.input.value = newEmail }, 92 | textStyle = textStyleField(), 93 | singleLine = true, 94 | modifier = Modifier 95 | .width(IntrinsicSize.Min) 96 | .padding(top = 12.dp, bottom = 12.dp), 97 | maxLines = 1, 98 | cursorBrush = SolidColor(SlackCloneColorProvider.colors.textPrimary), 99 | decorationBox = { inputTf -> 100 | Box { 101 | if (workspace.isEmpty()) { 102 | Text( 103 | text = "your-workspace", 104 | style = textStyleField(), 105 | textAlign = TextAlign.Start, 106 | modifier = Modifier.width(IntrinsicSize.Max), 107 | ) 108 | } else { 109 | inputTf() 110 | } 111 | } 112 | } 113 | ) 114 | } 115 | 116 | @Composable 117 | private fun textStyleField() = SlackCloneTypography.h6.copy( 118 | color = SlackCloneColorProvider.colors.textPrimary.copy(alpha = 0.7f), 119 | fontWeight = FontWeight.Normal, 120 | textAlign = TextAlign.Start 121 | ) 122 | -------------------------------------------------------------------------------- /feat-onboarding/src/main/java/io/getstream/slackclone/uionboarding/nav/OnboardingNavigation.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uionboarding.nav 2 | 3 | import androidx.hilt.navigation.compose.hiltViewModel 4 | import androidx.navigation.NavGraphBuilder 5 | import androidx.navigation.compose.composable 6 | import androidx.navigation.navigation 7 | import io.getstream.slackclone.navigator.ComposeNavigator 8 | import io.getstream.slackclone.navigator.SlackRoute 9 | import io.getstream.slackclone.navigator.SlackScreen 10 | import io.getstream.slackclone.uionboarding.compose.EmailAddressInputUI 11 | import io.getstream.slackclone.uionboarding.compose.GettingStartedUI 12 | import io.getstream.slackclone.uionboarding.compose.SkipTypingUI 13 | import io.getstream.slackclone.uionboarding.compose.WorkspaceInputUI 14 | 15 | fun NavGraphBuilder.onboardingNavigation( 16 | composeNavigator: ComposeNavigator 17 | ) { 18 | navigation( 19 | startDestination = SlackScreen.GettingStarted.name, 20 | route = SlackRoute.OnBoarding.name 21 | ) { 22 | composable(SlackScreen.GettingStarted.name) { 23 | GettingStartedUI(composeNavigator) 24 | } 25 | composable(SlackScreen.SkipTypingScreen.name) { 26 | SkipTypingUI(composeNavigator) 27 | } 28 | composable(SlackScreen.WorkspaceInputUI.name) { 29 | WorkspaceInputUI(composeNavigator, hiltViewModel()) 30 | } 31 | composable(SlackScreen.EmailAddressInputUI.name) { 32 | EmailAddressInputUI(composeNavigator, hiltViewModel()) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /feat-onboarding/src/main/res/drawable-hdpi/gettingstarted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/feat-onboarding/src/main/res/drawable-hdpi/gettingstarted.png -------------------------------------------------------------------------------- /feat-onboarding/src/main/res/drawable-xxhdpi/gettingstarted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/feat-onboarding/src/main/res/drawable-xxhdpi/gettingstarted.png -------------------------------------------------------------------------------- /feat-onboarding/src/main/res/drawable-xxxhdpi/gettingstarted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/feat-onboarding/src/main/res/drawable-xxxhdpi/gettingstarted.png -------------------------------------------------------------------------------- /feat-onboarding/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | This is the address you use to sign in to Slack 4 | We\'ll send you an email that\'ll instantly sign you in.ø 5 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.configureondemand=true 10 | org.gradle.caching=true 11 | org.gradle.parallel=true 12 | # Ensure important default jvmargs aren't overwritten. See https://github.com/gradle/gradle/issues/19750 13 | org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | # AndroidX package structure to make it clearer which packages are bundled with the 19 | # Android operating system, and which are packaged with your app"s APK 20 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 21 | android.useAndroidX=true 22 | # Kotlin code style for this project: "official" or "obsolete": 23 | kotlin.code.style=official 24 | # Allow kapt to use incremental processing 25 | kapt.incremental.apt=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Oct 15 09:35:41 KST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /navigator/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /navigator/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_KSP) 5 | id(BuildPlugins.COMPOSE_COMPILER) 6 | } 7 | 8 | android { 9 | compileSdk = ProjectProperties.COMPILE_SDK 10 | namespace = "io.getstream.slackclone.navigator" 11 | 12 | defaultConfig { 13 | minSdk = (ProjectProperties.MIN_SDK) 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | 17 | buildTypes { 18 | getByName("release") { 19 | isMinifyEnabled = false 20 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 21 | } 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility = JavaVersion.VERSION_1_8 26 | targetCompatibility = JavaVersion.VERSION_1_8 27 | } 28 | 29 | kotlinOptions { 30 | jvmTarget = "1.8" 31 | } 32 | } 33 | 34 | dependencies { 35 | /*Kotlin*/ 36 | implementation(Lib.Android.APP_COMPAT) 37 | implementation(Lib.Kotlin.KTX_CORE) 38 | api(Lib.Async.COROUTINES) 39 | api(Lib.Async.COROUTINES_ANDROID) 40 | 41 | implementation(Lib.Kotlin.KT_STD) 42 | implementation(Lib.Android.COMPOSE_NAVIGATION) 43 | 44 | implementation(Lib.Android.COMPOSE_NAVIGATION) 45 | implementation(Lib.Di.hiltNavigationCompose) 46 | } -------------------------------------------------------------------------------- /navigator/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /navigator/src/main/java/io/getstream/slackclone/navigator/NavigationCommand.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.navigator 2 | 3 | import androidx.navigation.NavOptions 4 | 5 | sealed class NavigationCommand { 6 | object NavigateUp : NavigationCommand() 7 | } 8 | 9 | sealed class ComposeNavigationCommand : NavigationCommand() { 10 | data class NavigateToRoute(val route: String, val options: NavOptions? = null) : 11 | ComposeNavigationCommand() 12 | 13 | data class NavigateUpWithResult( 14 | val key: String, 15 | val result: T, 16 | val route: String? = null 17 | ) : ComposeNavigationCommand() 18 | 19 | data class PopUpToRoute(val route: String, val inclusive: Boolean) : ComposeNavigationCommand() 20 | } 21 | -------------------------------------------------------------------------------- /navigator/src/main/java/io/getstream/slackclone/navigator/NavigationKeys.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.navigator 2 | 3 | object NavigationKeys { 4 | 5 | val navigateChannel = "ChannelCreated" 6 | } 7 | -------------------------------------------------------------------------------- /navigator/src/main/java/io/getstream/slackclone/navigator/Navigator.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.navigator 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.Observer 5 | import androidx.navigation.NavController 6 | import androidx.navigation.NavOptionsBuilder 7 | import kotlinx.coroutines.DelicateCoroutinesApi 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.GlobalScope 10 | import kotlinx.coroutines.channels.Channel 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.MutableSharedFlow 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.flow 15 | import kotlinx.coroutines.flow.onCompletion 16 | import kotlinx.coroutines.flow.onSubscription 17 | import kotlinx.coroutines.launch 18 | import kotlinx.coroutines.withContext 19 | 20 | abstract class Navigator { 21 | val navigationCommands = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) 22 | 23 | // We use a StateFlow here to allow ViewModels to start observing navigation results before the initial composition, 24 | // and still get the navigation result later 25 | val navControllerFlow = MutableStateFlow(null) 26 | 27 | fun navigateUp() { 28 | navigationCommands.tryEmit(NavigationCommand.NavigateUp) 29 | } 30 | } 31 | 32 | abstract class ComposeNavigator : Navigator() { 33 | abstract fun navigate(route: String, optionsBuilder: (NavOptionsBuilder.() -> Unit)? = null) 34 | abstract fun observeResult(key: String, route: String? = null): Flow 35 | abstract fun navigateBackWithResult(key: String, result: T, route: String?) 36 | 37 | abstract fun popUpTo(route: String, inclusive: Boolean) 38 | abstract fun navigateAndClearBackStack(route: String) 39 | 40 | suspend fun handleNavigationCommands(navController: NavController) { 41 | navigationCommands 42 | .onSubscription { this@ComposeNavigator.navControllerFlow.value = navController } 43 | .onCompletion { this@ComposeNavigator.navControllerFlow.value = null } 44 | .collect { navController.handleComposeNavigationCommand(it) } 45 | } 46 | 47 | private fun NavController.handleComposeNavigationCommand(navigationCommand: NavigationCommand) { 48 | when (navigationCommand) { 49 | is ComposeNavigationCommand.NavigateToRoute -> { 50 | navigate(navigationCommand.route, navigationCommand.options) 51 | } 52 | NavigationCommand.NavigateUp -> navigateUp() 53 | is ComposeNavigationCommand.PopUpToRoute -> popBackStack( 54 | navigationCommand.route, 55 | navigationCommand.inclusive 56 | ) 57 | is ComposeNavigationCommand.NavigateUpWithResult<*> -> { 58 | navUpWithResult(navigationCommand) 59 | } 60 | } 61 | } 62 | 63 | private fun NavController.navUpWithResult(navigationCommand: ComposeNavigationCommand.NavigateUpWithResult<*>) { 64 | val backStackEntry = 65 | navigationCommand.route?.let { getBackStackEntry(it) } 66 | ?: previousBackStackEntry 67 | backStackEntry?.savedStateHandle?.set( 68 | navigationCommand.key, 69 | navigationCommand.result 70 | ) 71 | 72 | navigationCommand.route?.let { 73 | popBackStack(it, false) 74 | } ?: run { 75 | navigateUp() 76 | } 77 | } 78 | } 79 | 80 | @OptIn(DelicateCoroutinesApi::class) 81 | fun LiveData.asFlow(): Flow = flow { 82 | val channel = Channel(Channel.CONFLATED) 83 | val observer = Observer { 84 | channel.trySend(it) 85 | } 86 | withContext(Dispatchers.Main.immediate) { 87 | observeForever(observer) 88 | } 89 | try { 90 | for (value in channel) { 91 | emit(value) 92 | } 93 | } finally { 94 | GlobalScope.launch(Dispatchers.Main.immediate) { 95 | removeObserver(observer) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /navigator/src/main/java/io/getstream/slackclone/navigator/Screens.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.navigator 2 | 3 | import androidx.navigation.NamedNavArgument 4 | import androidx.navigation.NavType 5 | import androidx.navigation.navArgument 6 | 7 | sealed class SlackScreen( 8 | val route: String, 9 | val navArguments: List = emptyList() 10 | ) { 11 | val name: String = route.appendArguments(navArguments) 12 | 13 | // onboarding 14 | object GettingStarted : SlackScreen("gettingStarted") 15 | object SkipTypingScreen : SlackScreen("SkipTypingUI") 16 | object EmailAddressInputUI : SlackScreen("EmailAddressInputUI") 17 | object WorkspaceInputUI : SlackScreen("WorkspaceInputUI") 18 | 19 | // dashboard 20 | object Dashboard : SlackScreen( 21 | "Dashboard", 22 | navArguments = listOf(navArgument("channelId") { type = NavType.StringType }) 23 | ) { 24 | fun createRoute(channelId: String) = 25 | route.replace("{${navArguments.first().name}}", channelId) 26 | } 27 | 28 | object CreateChannelsScreen : SlackScreen("CreateChannelsScreen") 29 | object CreateNewChannel : SlackScreen("CreateNewChannel") 30 | object CreateNewDM : SlackScreen("CreateNewDM") 31 | } 32 | 33 | sealed class SlackRoute(val name: String) { 34 | object OnBoarding : SlackRoute("onboarding") 35 | object Dashboard : SlackRoute("dashboard") 36 | } 37 | 38 | private fun String.appendArguments(navArguments: List): String { 39 | val mandatoryArguments = navArguments.filter { it.argument.defaultValue == null } 40 | .takeIf { it.isNotEmpty() } 41 | ?.joinToString(separator = "/", prefix = "/") { "{${it.name}}" } 42 | .orEmpty() 43 | val optionalArguments = navArguments.filter { it.argument.defaultValue != null } 44 | .takeIf { it.isNotEmpty() } 45 | ?.joinToString(separator = "&", prefix = "?") { "${it.name}={${it.name}}" } 46 | .orEmpty() 47 | return "$this$mandatoryArguments$optionalArguments" 48 | } 49 | -------------------------------------------------------------------------------- /navigator/src/main/java/io/getstream/slackclone/navigator/SlackCloneComposeNavigator.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.navigator 2 | 3 | import androidx.navigation.NavOptionsBuilder 4 | import androidx.navigation.navOptions 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.emptyFlow 7 | import kotlinx.coroutines.flow.filter 8 | import kotlinx.coroutines.flow.filterNotNull 9 | import kotlinx.coroutines.flow.flatMapLatest 10 | import kotlinx.coroutines.flow.onEach 11 | import javax.inject.Inject 12 | 13 | class SlackCloneComposeNavigator @Inject constructor() : ComposeNavigator() { 14 | 15 | override fun navigate(route: String, optionsBuilder: (NavOptionsBuilder.() -> Unit)?) { 16 | val options = optionsBuilder?.let { navOptions(it) } 17 | navigationCommands.tryEmit(ComposeNavigationCommand.NavigateToRoute(route, options)) 18 | } 19 | 20 | override fun navigateAndClearBackStack(route: String) { 21 | navigationCommands.tryEmit( 22 | ComposeNavigationCommand.NavigateToRoute( 23 | route, 24 | navOptions { 25 | popUpTo(0) 26 | } 27 | ) 28 | ) 29 | } 30 | 31 | override fun popUpTo(route: String, inclusive: Boolean) { 32 | navigationCommands.tryEmit(ComposeNavigationCommand.PopUpToRoute(route, inclusive)) 33 | } 34 | 35 | override fun navigateBackWithResult( 36 | key: String, 37 | result: T, 38 | route: String? 39 | ) { 40 | navigationCommands.tryEmit( 41 | ComposeNavigationCommand.NavigateUpWithResult( 42 | key = key, 43 | result = result, 44 | route = route 45 | ) 46 | ) 47 | } 48 | 49 | override fun observeResult(key: String, route: String?): Flow { 50 | return navControllerFlow 51 | .filterNotNull() 52 | .flatMapLatest { navController -> 53 | val backStackEntry = route?.let { navController.getBackStackEntry(it) } 54 | ?: navController.currentBackStackEntry 55 | 56 | backStackEntry?.savedStateHandle?.let { savedStateHandle -> 57 | savedStateHandle.getLiveData(key) 58 | .asFlow() 59 | .filter { it != null } 60 | .onEach { 61 | // Nullify the result to avoid resubmitting it 62 | savedStateHandle.set(key, null) 63 | } as Flow 64 | } ?: emptyFlow() 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | // Root module 2 | include(":app") 3 | 4 | // Other modules 5 | include(":domain") 6 | include(":data") 7 | include(":common") 8 | include(":commonui") 9 | include(":navigator") 10 | 11 | // Feature modules 12 | include(":ui-dashboard") 13 | include(":feat-onboarding") 14 | include(":feat-chat") 15 | include(":feat-channels") 16 | include(":feat-chatcore") 17 | include(":benchmark") 18 | -------------------------------------------------------------------------------- /spotless.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.diffplug.spotless") 3 | } 4 | 5 | spotless { 6 | kotlin { 7 | target("**/*.kt") 8 | targetExclude("**/build/**/*.kt") 9 | ktlint().userData(mapOf("android" to "true", "indent_size" to "2", "continuation_indent_size" to 2)) 10 | trimTrailingWhitespace() 11 | endWithNewline() 12 | } 13 | 14 | format("kts") { 15 | target("**/*.kts") 16 | targetExclude("**/build/**/*.kts") 17 | } 18 | } -------------------------------------------------------------------------------- /team-props/git-hooks.gradle.kts: -------------------------------------------------------------------------------- 1 | fun isLinuxOrMacOs(): Boolean { 2 | val osName = System.getProperty("os.name") 3 | .toLowerCase() 4 | return osName.contains("linux") || osName.contains("mac os") || osName.contains("macos") 5 | } 6 | 7 | tasks.create("copyGitHooks") { 8 | description = "Copies the git hooks from team-props/git-hooks to the .git folder." 9 | from("$rootDir/team-props/git-hooks/") { 10 | include("**/*.sh") 11 | rename("(.*).sh", "$1") 12 | } 13 | into("$rootDir/.git/hooks") 14 | onlyIf { isLinuxOrMacOs() } 15 | } 16 | 17 | tasks.create("installGitHooks") { 18 | description = "Installs the pre-commit git hooks from team-props/git-hooks." 19 | group = "git hooks" 20 | workingDir(rootDir) 21 | commandLine("chmod") 22 | args("-R", "+x", ".git/hooks/") 23 | dependsOn("copyGitHooks") 24 | onlyIf { isLinuxOrMacOs() } 25 | doLast { 26 | logger.info("Git hook installed successfully.") 27 | } 28 | } 29 | 30 | tasks.getByName("installGitHooks") 31 | .dependsOn(getTasksByName("copyGitHooks", true)) 32 | tasks.getByPath("app:preBuild") 33 | .dependsOn(getTasksByName("installGitHooks", true)) 34 | -------------------------------------------------------------------------------- /team-props/git-hooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Running code formatting with spotless..." 4 | 5 | ./gradlew spotlessApply 6 | 7 | status=$? 8 | 9 | if [ "$status" = 0 ] ; then 10 | echo "Code formatting success." 11 | exit 0 12 | else 13 | echo 1>&2 "Static analysis found violations it could not fix." 14 | exit 1 15 | fi -------------------------------------------------------------------------------- /ui-dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /ui-dashboard/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_KSP) 5 | id(BuildPlugins.DAGGER_HILT) 6 | id(BuildPlugins.KOTLIN_PARCELABLE_PLUGIN) 7 | id(BuildPlugins.COMPOSE_COMPILER) 8 | } 9 | 10 | android { 11 | compileSdk = ProjectProperties.COMPILE_SDK 12 | namespace = "io.getstream.slackclone.uidashboard" 13 | 14 | defaultConfig { 15 | minSdk = (ProjectProperties.MIN_SDK) 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | getByName("release") { 21 | isMinifyEnabled = false 22 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 23 | } 24 | } 25 | 26 | buildFeatures { 27 | compose = true 28 | } 29 | 30 | packaging { 31 | resources.excludes.add("META-INF/LICENSE.txt") 32 | resources.excludes.add("META-INF/NOTICE.txt") 33 | resources.excludes.add("LICENSE.txt") 34 | resources.excludes.add("/META-INF/{AL2.0,LGPL2.1}") 35 | } 36 | 37 | compileOptions { 38 | sourceCompatibility = JavaVersion.VERSION_1_8 39 | targetCompatibility = JavaVersion.VERSION_1_8 40 | } 41 | 42 | kotlinOptions { 43 | jvmTarget = "1.8" 44 | } 45 | 46 | } 47 | 48 | dependencies { 49 | implementation(project(":feat-chat")) 50 | implementation(project(":feat-channels")) 51 | api(project(":feat-chatcore")) 52 | 53 | implementation(project(":data")) 54 | implementation(project(":domain")) 55 | implementation(project(":common")) 56 | implementation(project(":navigator")) 57 | implementation(project(":commonui")) 58 | 59 | api(Lib.Android.COMPOSE_UI) 60 | api(Lib.Android.COMPOSE_MATERIAL) 61 | implementation(Lib.Android.ACCOMPANIST_SYSTEM_UI_CONTROLLER) 62 | api(Lib.Android.COMPOSE_UI) 63 | api(Lib.Android.COMPOSE_TOOLING) 64 | implementation(Lib.Android.LANDSCAPIST_GLIDE) 65 | debugApi(Lib.Android.COMPOSE_DEBUG_TOOLING) 66 | api(Lib.Android.ACTIVITY_COMPOSE) 67 | api(Lib.Android.CONSTRAINT_LAYOUT_COMPOSE) 68 | 69 | api(Lib.Android.APP_COMPAT) 70 | api(Lib.Kotlin.KTX_CORE) 71 | 72 | /*DI*/ 73 | api(Lib.Di.hiltAndroid) 74 | api(Lib.Di.hiltNavigationCompose) 75 | 76 | ksp(Lib.Di.hiltCompiler) 77 | ksp(Lib.Di.hiltAndroidCompiler) 78 | 79 | /* Logger */ 80 | api(Lib.Logger.TIMBER) 81 | /* Async */ 82 | api(Lib.Async.COROUTINES) 83 | api(Lib.Async.COROUTINES_ANDROID) 84 | 85 | testImplementation(TestLib.JUNIT) 86 | testImplementation(TestLib.CORE_TEST) 87 | testImplementation(TestLib.ANDROID_JUNIT) 88 | testImplementation(TestLib.ARCH_CORE) 89 | testImplementation(TestLib.MOCK_WEB_SERVER) 90 | testImplementation(TestLib.ROBO_ELECTRIC) 91 | testImplementation(TestLib.COROUTINES) 92 | testImplementation(TestLib.MOCKK) 93 | testImplementation(TestLib.TURBINE) 94 | } -------------------------------------------------------------------------------- /ui-dashboard/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /ui-dashboard/src/main/java/io/getstream/slackclone/uidashboard/compose/DashboardVM.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uidashboard.compose 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import dagger.hilt.android.lifecycle.HiltViewModel 7 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 8 | import io.getstream.slackclone.domain.mappers.UiModelMapper 9 | import io.getstream.slackclone.domain.model.channel.DomainLayerChannels 10 | import io.getstream.slackclone.domain.usecases.channels.UseCaseCreateLocalChannels 11 | import io.getstream.slackclone.domain.usecases.channels.UseCaseGetChannel 12 | import io.getstream.slackclone.domain.usecases.users.UseCaseFetchUsers 13 | import io.getstream.slackclone.domain.usecases.users.UseCaseLogoutUser 14 | import io.getstream.slackclone.navigator.ComposeNavigator 15 | import io.getstream.slackclone.navigator.NavigationKeys 16 | import kotlinx.coroutines.flow.MutableStateFlow 17 | import kotlinx.coroutines.flow.launchIn 18 | import kotlinx.coroutines.flow.map 19 | import kotlinx.coroutines.flow.onEach 20 | import kotlinx.coroutines.flow.onStart 21 | import kotlinx.coroutines.launch 22 | import javax.inject.Inject 23 | 24 | @HiltViewModel 25 | class DashboardVM @Inject constructor( 26 | private val savedStateHandle: SavedStateHandle, 27 | private val composeNavigator: ComposeNavigator, 28 | private val useCaseGetChannel: UseCaseGetChannel, 29 | private val useCaseFetchUsers: UseCaseFetchUsers, 30 | private val useCaseLogoutUser: UseCaseLogoutUser, 31 | private val useCaseSaveChannel: UseCaseCreateLocalChannels, 32 | private val channelMapper: UiModelMapper 33 | ) : ViewModel() { 34 | 35 | val selectedChatChannel = MutableStateFlow(null) 36 | val isChatViewClosed = MutableStateFlow(true) 37 | 38 | init { 39 | observeChannelCreated() 40 | preloadUsers() 41 | } 42 | 43 | private fun observeChannelCreated() { 44 | composeNavigator.observeResult( 45 | NavigationKeys.navigateChannel, 46 | ).onStart { 47 | val message = savedStateHandle.get(NavigationKeys.navigateChannel) 48 | message?.let { 49 | emit(it) 50 | } 51 | }.map { 52 | useCaseGetChannel.perform(it) 53 | }.onEach { slackChannel -> 54 | navigateChatThreadForChannel(slackChannel) 55 | } 56 | .launchIn(viewModelScope) 57 | 58 | selectedChatChannel.onEach { 59 | savedStateHandle.set(NavigationKeys.navigateChannel, it?.uuid) 60 | }.launchIn(viewModelScope) 61 | } 62 | 63 | private fun navigateChatThreadForChannel(slackChannel: DomainLayerChannels.SlackChannel?) { 64 | slackChannel?.let { 65 | selectedChatChannel.value = channelMapper.mapToPresentation(it) 66 | isChatViewClosed.value = false 67 | } 68 | } 69 | 70 | private fun preloadUsers() { 71 | viewModelScope.launch { 72 | val users = useCaseFetchUsers.perform(10) 73 | useCaseSaveChannel.perform(users) 74 | } 75 | } 76 | 77 | fun logout() { 78 | viewModelScope.launch { 79 | useCaseLogoutUser.perform() 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ui-dashboard/src/main/java/io/getstream/slackclone/uidashboard/home/DirectMessagesUI.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uidashboard.home 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.text.font.FontWeight 12 | import androidx.compose.ui.unit.dp 13 | import io.getstream.slackclone.chatcore.data.UiLayerChannels 14 | import io.getstream.slackclone.commonui.material.SlackSurfaceAppBar 15 | import io.getstream.slackclone.commonui.theme.SlackCloneColorProvider 16 | import io.getstream.slackclone.commonui.theme.SlackCloneSurface 17 | import io.getstream.slackclone.commonui.theme.SlackCloneTypography 18 | import io.getstream.slackclone.uichannels.directmessages.DMChannelsList 19 | 20 | @Composable 21 | fun DirectMessagesUI(onItemClick: (UiLayerChannels.SlackChannel) -> Unit) { 22 | SlackCloneSurface( 23 | color = SlackCloneColorProvider.colors.uiBackground, 24 | modifier = Modifier.fillMaxSize() 25 | ) { 26 | Column { 27 | DMTopAppBar() 28 | Spacer(modifier = Modifier.height(8.dp)) 29 | JumpToText() 30 | Spacer(modifier = Modifier.height(12.dp)) 31 | DMChannelsList(onItemClick) 32 | } 33 | } 34 | } 35 | 36 | @Composable 37 | fun DMTopAppBar() { 38 | SlackSurfaceAppBar( 39 | title = { 40 | Text( 41 | text = "Direct Messages", 42 | style = SlackCloneTypography.h5.copy(color = Color.White, fontWeight = FontWeight.Bold) 43 | ) 44 | }, 45 | backgroundColor = SlackCloneColorProvider.colors.appBarColor, 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /ui-dashboard/src/main/java/io/getstream/slackclone/uidashboard/home/MentionsReactionsUI.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uidashboard.home 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.res.painterResource 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.compose.ui.text.font.FontWeight 12 | import io.getstream.chat.android.compose.ui.theme.ChatTheme 13 | import io.getstream.slackclone.commonui.material.SlackSurfaceAppBar 14 | import io.getstream.slackclone.commonui.theme.SlackCloneColorProvider 15 | import io.getstream.slackclone.commonui.theme.SlackCloneSurface 16 | import io.getstream.slackclone.commonui.theme.SlackCloneTypography 17 | import io.getstream.slackclone.uichannels.directmessages.EmptyContent 18 | import io.getstream.slackclone.uidashboard.R 19 | 20 | @Composable 21 | fun MentionsReactionsUI() { 22 | SlackCloneSurface( 23 | color = SlackCloneColorProvider.colors.uiBackground, 24 | modifier = Modifier.fillMaxSize() 25 | ) { 26 | Column { 27 | MRTopAppBar() 28 | ChatTheme { 29 | EmptyContent( 30 | modifier = Modifier.fillMaxSize(), 31 | painter = painterResource(id = io.getstream.chat.android.compose.R.drawable.stream_compose_empty_channels), 32 | text = stringResource(io.getstream.chat.android.compose.R.string.stream_compose_channel_list_empty_channels), 33 | ) 34 | } 35 | } 36 | } 37 | } 38 | 39 | @Composable 40 | private fun MRTopAppBar() { 41 | SlackSurfaceAppBar( 42 | title = { 43 | Text( 44 | text = "Mentions & Reactions", 45 | style = SlackCloneTypography.h5.copy(color = Color.White, fontWeight = FontWeight.Bold) 46 | ) 47 | }, 48 | backgroundColor = SlackCloneColorProvider.colors.appBarColor, 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /ui-dashboard/src/main/java/io/getstream/slackclone/uidashboard/home/SearchMessagesUI.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uidashboard.home 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.foundation.verticalScroll 9 | import androidx.compose.material.Divider 10 | import androidx.compose.material.Text 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.filled.Clear 13 | import androidx.compose.material.icons.filled.Favorite 14 | import androidx.compose.material.icons.filled.Search 15 | import androidx.compose.material.icons.filled.ShoppingCart 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.text.font.FontWeight 20 | import androidx.compose.ui.unit.dp 21 | import io.getstream.slackclone.commonui.material.SlackSurfaceAppBar 22 | import io.getstream.slackclone.commonui.reusable.SlackListItem 23 | import io.getstream.slackclone.commonui.theme.SlackCloneColorProvider 24 | import io.getstream.slackclone.commonui.theme.SlackCloneSurface 25 | import io.getstream.slackclone.commonui.theme.SlackCloneTypography 26 | import io.getstream.slackclone.uidashboard.R 27 | import io.getstream.slackclone.uidashboard.home.search.SearchCancel 28 | 29 | @Composable 30 | fun SearchMessagesUI() { 31 | SlackCloneSurface( 32 | color = SlackCloneColorProvider.colors.uiBackground, 33 | modifier = Modifier.fillMaxSize() 34 | ) { 35 | Column { 36 | SearchTopAppBar() 37 | Content() 38 | } 39 | } 40 | } 41 | 42 | @Composable 43 | private fun SearchTopAppBar() { 44 | SlackSurfaceAppBar( 45 | backgroundColor = SlackCloneColorProvider.colors.appBarColor, 46 | contentPadding = PaddingValues(8.dp) 47 | ) { 48 | SearchCancel() 49 | } 50 | } 51 | 52 | @Composable 53 | private fun Content() { 54 | Column(Modifier.verticalScroll(rememberScrollState())) { 55 | SlackListItem( 56 | icon = Icons.Default.ShoppingCart, 57 | title = stringResource(io.getstream.slackclone.common.R.string.browse_people) 58 | ) 59 | SlackListItem( 60 | icon = Icons.Default.Search, 61 | title = stringResource(io.getstream.slackclone.common.R.string.browse_channels) 62 | ) 63 | SlackListDivider() 64 | // Recent Searches 65 | SearchText(stringResource(io.getstream.slackclone.common.R.string.recent_searches)) 66 | repeat(5) { 67 | SlackListItem( 68 | icon = Icons.Default.Favorite, 69 | title = "in:#android_india", 70 | trailingItem = Icons.Default.Clear 71 | ) 72 | } 73 | SlackListDivider() 74 | // Narrow Your Search 75 | SearchText(stringResource(io.getstream.slackclone.common.R.string.narrow_your_search)) 76 | repeat(5) { 77 | SlackListItemTrailingView( 78 | icon = Icons.Default.Favorite, 79 | title = "from:", 80 | trailingView = { 81 | Text(text = "Ex: @zoemaxwell") 82 | } 83 | ) 84 | } 85 | } 86 | } 87 | 88 | @Composable 89 | private fun SearchText(title: String) { 90 | Text( 91 | text = title, 92 | style = SlackCloneTypography.caption.copy(fontWeight = FontWeight.SemiBold), 93 | modifier = Modifier.padding(16.dp) 94 | ) 95 | } 96 | 97 | @Composable 98 | fun SlackListDivider() { 99 | Divider(color = SlackCloneColorProvider.colors.lineColor, thickness = 0.5.dp) 100 | } 101 | -------------------------------------------------------------------------------- /ui-dashboard/src/main/java/io/getstream/slackclone/uidashboard/home/search/SearchCancel.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uidashboard.home.search 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.foundation.text.BasicTextField 9 | import androidx.compose.material.Icon 10 | import androidx.compose.material.Text 11 | import androidx.compose.material.TextButton 12 | import androidx.compose.material.icons.Icons 13 | import androidx.compose.material.icons.filled.Search 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.setValue 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.ExperimentalComposeUiApi 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.graphics.Color 23 | import androidx.compose.ui.graphics.SolidColor 24 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 25 | import androidx.compose.ui.unit.dp 26 | import io.getstream.slackclone.commonui.keyboard.Keyboard 27 | import io.getstream.slackclone.commonui.keyboard.keyboardAsState 28 | import io.getstream.slackclone.commonui.theme.SlackCloneTypography 29 | 30 | @OptIn(ExperimentalComposeUiApi::class) 31 | @Composable 32 | fun SearchCancel() { 33 | val keyboardController = LocalSoftwareKeyboardController.current 34 | 35 | Row { 36 | val isKeyboardOpen by keyboardAsState() 37 | var search by remember { mutableStateOf("") } 38 | 39 | SearchMessagesTF(modifier = Modifier.weight(1f), search) { newValue -> 40 | search = newValue 41 | } 42 | AnimatedVisibility(visible = isKeyboardOpen is Keyboard.Opened) { 43 | TextButton(onClick = { 44 | search = "" 45 | keyboardController?.hide() 46 | }) { 47 | Text( 48 | "Cancel", 49 | style = SlackCloneTypography.subtitle1.copy(color = Color.White) 50 | ) 51 | } 52 | } 53 | } 54 | } 55 | 56 | @Composable 57 | private fun SearchMessagesTF(modifier: Modifier, search: String, onValueChange: (String) -> Unit) { 58 | BasicTextField( 59 | value = search, 60 | singleLine = true, 61 | maxLines = 1, 62 | onValueChange = { newSearch -> 63 | onValueChange(newSearch) 64 | }, 65 | textStyle = SlackCloneTypography.subtitle1.copy( 66 | color = Color.White, 67 | ), 68 | decorationBox = { innerTextField -> 69 | Row( 70 | Modifier 71 | .background( 72 | color = Color.White.copy(alpha = 0.2f), 73 | shape = RoundedCornerShape(12.dp) 74 | ) 75 | .padding(8.dp), 76 | verticalAlignment = Alignment.CenterVertically 77 | ) { 78 | Icon( 79 | imageVector = Icons.Default.Search, 80 | contentDescription = null, 81 | tint = Color.White 82 | ) 83 | if (search.isEmpty()) { 84 | Text( 85 | "Search for messages and files", 86 | style = SlackCloneTypography.subtitle1.copy( 87 | color = Color.White, 88 | ), 89 | modifier = Modifier.weight(1f) 90 | ) 91 | } else { 92 | innerTextField() 93 | } 94 | } 95 | }, 96 | modifier = modifier.padding(8.dp), 97 | cursorBrush = SolidColor(Color.White), 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /ui-dashboard/src/main/java/io/getstream/slackclone/uidashboard/nav/dashboardNavigation.kt: -------------------------------------------------------------------------------- 1 | package io.getstream.slackclone.uidashboard.nav 2 | 3 | import androidx.navigation.NavGraphBuilder 4 | import androidx.navigation.compose.composable 5 | import androidx.navigation.navigation 6 | import io.getstream.slackclone.navigator.ComposeNavigator 7 | import io.getstream.slackclone.navigator.SlackRoute 8 | import io.getstream.slackclone.navigator.SlackScreen 9 | import io.getstream.slackclone.uichannels.createsearch.CreateNewChannelUI 10 | import io.getstream.slackclone.uichannels.createsearch.SearchCreateChannelUI 11 | import io.getstream.slackclone.uichat.newchat.NewChatThreadScreen 12 | import io.getstream.slackclone.uidashboard.compose.DashboardUI 13 | 14 | fun NavGraphBuilder.dashboardNavigation( 15 | composeNavigator: ComposeNavigator, 16 | ) { 17 | navigation( 18 | startDestination = SlackScreen.Dashboard.name, 19 | route = SlackRoute.Dashboard.name 20 | ) { 21 | composable(SlackScreen.Dashboard.name) { 22 | DashboardUI(composeNavigator) 23 | } 24 | composable(SlackScreen.CreateChannelsScreen.name) { 25 | SearchCreateChannelUI(composeNavigator = composeNavigator) 26 | } 27 | composable(SlackScreen.CreateNewChannel.name) { 28 | CreateNewChannelUI(composeNavigator) 29 | } 30 | composable(SlackScreen.CreateNewDM.name) { 31 | NewChatThreadScreen(composeNavigator) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui-dashboard/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MainActivity 3 | 4 | --------------------------------------------------------------------------------