├── .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 |
11 |
--------------------------------------------------------------------------------
/app/src/test/resources/responses/jokes_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "success",
3 | "value": [
4 | {
5 | "id": 427,
6 | "joke": "Chuck Norris' favorite cereal is Kellogg's Nails 'N' Gravel.",
7 | "categories": []
8 | },
9 | {
10 | "id": 75,
11 | "joke": "Chuck Norris can believe it's not butter.",
12 | "categories": []
13 | },
14 | {
15 | "id": 302,
16 | "joke": "Chuck Norris doesn't go on the internet, he has every internet site stored in his memory. He refreshes webpages by blinking.",
17 | "categories": []
18 | },
19 | {
20 | "id": 275,
21 | "joke": "Little Miss Muffet sat on her tuffet, until Chuck Norris roundhouse kicked her into a glacier.",
22 | "categories": []
23 | },
24 | {
25 | "id": 76,
26 | "joke": "If tapped, a Chuck Norris roundhouse kick could power the country of Australia for 44 minutes.",
27 | "categories": []
28 | }
29 | ]
30 | }
--------------------------------------------------------------------------------
/art/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/art/architecture.png
--------------------------------------------------------------------------------
/art/art0.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/art/art0.gif
--------------------------------------------------------------------------------
/art/art1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/art/art1.png
--------------------------------------------------------------------------------
/art/art2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/art/art2.png
--------------------------------------------------------------------------------
/art/art3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/art/art3.png
--------------------------------------------------------------------------------
/art/art4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/art/art4.png
--------------------------------------------------------------------------------
/art/art5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/art/art5.png
--------------------------------------------------------------------------------
/art/art6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/art/art6.png
--------------------------------------------------------------------------------
/art/art7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/art/art7.png
--------------------------------------------------------------------------------
/art/art8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/art/art8.png
--------------------------------------------------------------------------------
/benchmark/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/benchmark/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(BuildPlugins.ANDROID_TEST_PLUGIN)
3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN)
4 | }
5 |
6 | java {
7 | sourceCompatibility = JavaVersion.VERSION_1_7
8 | targetCompatibility = JavaVersion.VERSION_1_7
9 | }
10 |
11 | android {
12 | namespace = "io.getstream.slackclone.benchmark"
13 | compileSdk = ProjectProperties.COMPILE_SDK
14 |
15 | defaultConfig {
16 | minSdk = 23
17 | targetSdk = (ProjectProperties.TARGET_SDK)
18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | // This benchmark buildType is used for benchmarking, and should function like your
23 | // release build (for example, with minification on). It's signed with a debug key
24 | // for easy local/CI testing.
25 | val benchmark by creating {
26 | isDebuggable = true
27 | signingConfig = signingConfigs.getByName("debug")
28 | matchingFallbacks.add("release")
29 | }
30 | }
31 |
32 | targetProjectPath = ":app"
33 | experimentalProperties["android.experimental.self-instrumenting"] = true
34 | }
35 |
36 | dependencies {
37 | implementation(TestLib.ANDROIDX_TEST_RUNNER)
38 | implementation(TestLib.ANDROIDX_TEST_RULES)
39 | implementation(TestLib.CORE_TEST)
40 | implementation(TestLib.MACRO_BENCHMARK)
41 | implementation(TestLib.BASE_PROFILE)
42 | implementation(TestLib.BASE_PROFILE)
43 | implementation(TestLib.ANDROIDX_UI_AUTOMATOR)
44 | }
45 |
46 | androidComponents {
47 | beforeVariants {
48 | it.enable = it.buildType == "benchmark"
49 | }
50 | }
--------------------------------------------------------------------------------
/benchmark/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/benchmark/src/main/java/io/getstream/slackclone/benchmark/baseprofile/BaselineProfileGenerator.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.benchmark.baseprofile
2 |
3 | import androidx.benchmark.macro.ExperimentalBaselineProfilesApi
4 | import androidx.benchmark.macro.junit4.BaselineProfileRule
5 | import androidx.test.uiautomator.By
6 | import org.junit.Rule
7 | import org.junit.Test
8 |
9 | /**
10 | * Generates a baseline profile which can be copied to `app/src/main/baseline-prof.txt`.
11 | */
12 | @ExperimentalBaselineProfilesApi
13 | class BaselineProfileGenerator {
14 | @get:Rule
15 | val baselineProfileRule = BaselineProfileRule()
16 |
17 | @Test
18 | fun startup() =
19 | baselineProfileRule.collectBaselineProfile(
20 | packageName = "io.getstream.slackclone"
21 | ) {
22 | pressHome()
23 | // This block defines the app's critical user journey. Here we are interested in
24 | // optimizing for app startup. But you can also navigate and scroll
25 | // through your most important UI.
26 | startActivityAndWait()
27 | device.waitForIdle()
28 |
29 | device.run {
30 | findObject(By.text("Slack"))
31 | waitForIdle()
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/benchmark/src/main/java/io/getstream/slackclone/benchmark/startup/StartupBenchmark.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.benchmark.startup
2 |
3 | import androidx.benchmark.macro.BaselineProfileMode.Disable
4 | import androidx.benchmark.macro.BaselineProfileMode.Require
5 | import androidx.benchmark.macro.CompilationMode
6 | import androidx.benchmark.macro.StartupMode
7 | import androidx.benchmark.macro.StartupMode.COLD
8 | import androidx.benchmark.macro.StartupMode.HOT
9 | import androidx.benchmark.macro.StartupMode.WARM
10 | import androidx.benchmark.macro.StartupTimingMetric
11 | import androidx.benchmark.macro.junit4.MacrobenchmarkRule
12 | import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
13 | import org.junit.Rule
14 | import org.junit.Test
15 | import org.junit.runner.RunWith
16 |
17 | /**
18 | * Run this benchmark from Studio to see startup measurements, and captured system traces
19 | * for investigating your app's performance from a cold state.
20 | */
21 | @RunWith(AndroidJUnit4ClassRunner::class)
22 | class ColdStartupBenchmark : AbstractStartupBenchmark(COLD)
23 |
24 | /**
25 | * Run this benchmark from Studio to see startup measurements, and captured system traces
26 | * for investigating your app's performance from a warm state.
27 | */
28 | @RunWith(AndroidJUnit4ClassRunner::class)
29 | class WarmStartupBenchmark : AbstractStartupBenchmark(WARM)
30 |
31 | /**
32 | * Run this benchmark from Studio to see startup measurements, and captured system traces
33 | * for investigating your app's performance from a hot state.
34 | */
35 | @RunWith(AndroidJUnit4ClassRunner::class)
36 | class HotStartupBenchmark : AbstractStartupBenchmark(HOT)
37 |
38 | /**
39 | * Base class for benchmarks with different startup modes.
40 | * Enables app startups from various states of baseline profile or [CompilationMode]s.
41 | */
42 | abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
43 | @get:Rule
44 | val benchmarkRule = MacrobenchmarkRule()
45 |
46 | @Test
47 | fun startupNoCompilation() = startup(CompilationMode.None())
48 |
49 | @Test
50 | fun startupBaselineProfileDisabled() = startup(
51 | CompilationMode.Partial(baselineProfileMode = Disable, warmupIterations = 1)
52 | )
53 |
54 | @Test
55 | fun startupBaselineProfile() = startup(CompilationMode.Partial(baselineProfileMode = Require))
56 |
57 | @Test
58 | fun startupFullCompilation() = startup(CompilationMode.Full())
59 |
60 | private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
61 | packageName = "io.getstream.slackclone",
62 | metrics = listOf(StartupTimingMetric()),
63 | compilationMode = compilationMode,
64 | iterations = 5,
65 | startupMode = startupMode,
66 | setupBlock = {
67 | pressHome()
68 | }
69 | ) {
70 | startActivityAndWait()
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | google()
6 | maven("https://plugins.gradle.org/m2/")
7 | }
8 | dependencies {
9 | classpath(BuildPlugins.TOOLS_BUILD_GRADLE)
10 | classpath(BuildPlugins.DAGGER_HILT_PLUGIN)
11 | classpath(BuildPlugins.KOTLIN_GRADLE_PLUGIN)
12 | classpath(BuildPlugins.COMPOSE_GRADLE_PLUGIN)
13 | classpath(BuildPlugins.KOTLIN_KSP_GRADLE_PLUGIN)
14 | classpath(kotlin("serialization", version = Lib.Kotlin.KOTLIN_VERSION))
15 | classpath(BuildPlugins.SPOTLESS)
16 | }
17 | }
18 |
19 | allprojects {
20 | apply {
21 | plugin("com.diffplug.spotless")
22 | }
23 |
24 | repositories {
25 | google()
26 | mavenCentral()
27 | }
28 |
29 | tasks.withType().all {
30 | kotlinOptions.freeCompilerArgs += listOf(
31 | "-Xopt-in=kotlin.RequiresOptIn",
32 | "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
33 | )
34 | kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
35 | }
36 | }
37 |
38 | tasks.register("clean")
39 | .configure {
40 | delete(rootProject.buildDir)
41 | }
42 |
43 | apply(from = teamPropsFile("git-hooks.gradle.kts"))
44 |
45 | fun teamPropsFile(propsFile: String): File {
46 | val teamPropsDir = file("team-props")
47 | return File(teamPropsDir, propsFile)
48 | }
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | repositories {
2 | jcenter()
3 | }
4 |
5 | plugins {
6 | `kotlin-dsl`
7 | }
8 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/ProjectProperties.kt:
--------------------------------------------------------------------------------
1 | object ProjectProperties {
2 | const val COMPILE_SDK = 34
3 | const val MIN_SDK = 21
4 | const val TARGET_SDK = 34
5 | const val APPLICATION_ID = "io.getstream.slackclone"
6 | }
7 |
--------------------------------------------------------------------------------
/common/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/common/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.common"
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 | buildFeatures {
25 | buildConfig = true
26 | }
27 | }
28 |
29 | dependencies {
30 | /*Kotlin*/
31 | api(Lib.Kotlin.KT_STD)
32 |
33 | /*Logger*/
34 | implementation(Lib.Android.APP_STARTUP)
35 | api(Lib.Logger.TIMBER)
36 |
37 | /* Dependency Injection */
38 | api(Lib.Di.hiltAndroid)
39 | api(Lib.Di.hiltNavigationCompose)
40 |
41 | ksp(Lib.Di.hiltCompiler)
42 | kspTest(Lib.Di.hiltCompiler)
43 | ksp(Lib.Di.hiltAndroidCompiler)
44 | kspTest(Lib.Di.hiltAndroidCompiler)
45 | }
--------------------------------------------------------------------------------
/common/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
12 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/common/src/main/java/io/getstream/slackclone/common/extensions/PrimitiveExtensions.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.common.extensions
2 |
3 | import android.annotation.SuppressLint
4 | import java.text.SimpleDateFormat
5 | import java.util.Calendar
6 |
7 | fun Long.calendar(): Calendar {
8 | return Calendar.getInstance().apply {
9 | this.timeInMillis = this@calendar
10 | }
11 | }
12 |
13 | @SuppressLint("SimpleDateFormat")
14 | fun Calendar.formattedTime(): String {
15 | return SimpleDateFormat("hh:mm a").format(this.time)
16 | }
17 |
--------------------------------------------------------------------------------
/common/src/main/java/io/getstream/slackclone/common/injection/DispatcherModule.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.common.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.slackclone.common.injection.dispatcher.CoroutineDispatcherProvider
8 | import io.getstream.slackclone.common.injection.dispatcher.RealCoroutineDispatcherProvider
9 | import javax.inject.Singleton
10 |
11 | @InstallIn(SingletonComponent::class)
12 | @Module
13 | class DispatcherModule {
14 | @Provides
15 | @Singleton
16 | fun providesCoroutineDispatcher(): CoroutineDispatcherProvider {
17 | return RealCoroutineDispatcherProvider()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/common/src/main/java/io/getstream/slackclone/common/injection/dispatcher/CoroutineDispatcherProvider.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.common.injection.dispatcher
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 |
5 | interface CoroutineDispatcherProvider {
6 | val main: CoroutineDispatcher
7 | val io: CoroutineDispatcher
8 | val default: CoroutineDispatcher
9 | val unconfirmed: CoroutineDispatcher
10 | }
11 |
--------------------------------------------------------------------------------
/common/src/main/java/io/getstream/slackclone/common/injection/dispatcher/RealCoroutineDispatcherProvider.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.common.injection.dispatcher
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Dispatchers
5 |
6 | open class RealCoroutineDispatcherProvider : CoroutineDispatcherProvider {
7 | override val main: CoroutineDispatcher by lazy { Dispatchers.Main }
8 | override val io: CoroutineDispatcher by lazy { Dispatchers.IO }
9 | override val default: CoroutineDispatcher by lazy { Dispatchers.Default }
10 | override val unconfirmed: CoroutineDispatcher by lazy { Dispatchers.Unconfined }
11 | }
12 |
--------------------------------------------------------------------------------
/common/src/main/java/io/getstream/slackclone/common/startup/TimberInitializer.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.common.startup
2 |
3 | import android.content.Context
4 | import androidx.startup.Initializer
5 | import io.getstream.slackclone.common.BuildConfig
6 | import timber.log.Timber
7 |
8 | class TimberInitializer : Initializer {
9 |
10 | override fun create(context: Context) {
11 | if (BuildConfig.DEBUG) {
12 | Timber.plant(Timber.DebugTree())
13 | }
14 | }
15 |
16 | override fun dependencies(): List>> = emptyList()
17 | }
18 |
--------------------------------------------------------------------------------
/common/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Home
4 | DMs
5 | Mentions
6 | Search
7 | You
8 | Workspaces
9 | Add a workspace
10 | Preferences
11 | Logout
12 | Pause Notifications
13 | Set yoursel as away
14 | Saved items
15 | View profile
16 | Notifications
17 | Threads
18 | prj_jetpack_compose
19 | Recent
20 | Starred
21 | Direct messages
22 | Channels
23 | Connections
24 | Browse People
25 | Browse Channels
26 | Recent Searches
27 | Narrow Your Search
28 |
--------------------------------------------------------------------------------
/commonui/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/commonui/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN)
3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN)
4 | id(BuildPlugins.COMPOSE_COMPILER)
5 | id(BuildPlugins.KOTLIN_KSP)
6 | }
7 |
8 | android {
9 | compileSdk = ProjectProperties.COMPILE_SDK
10 | namespace = "io.getstream.slackclone.commonui"
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 | buildFeatures {
24 | compose = true
25 | }
26 |
27 | packaging {
28 | resources {
29 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
30 | }
31 | }
32 | }
33 |
34 | dependencies {
35 | /*Kotlin*/
36 | api(Lib.Kotlin.KT_STD)
37 | api(Lib.Kotlin.KTX_CORE)
38 | /* Android Designing and layout */
39 | api(Lib.Android.MATERIAL_DESIGN)
40 | api(Lib.Android.COMPOSE_UI)
41 | implementation(Lib.Android.CONSTRAINT_LAYOUT_COMPOSE)
42 | implementation(Lib.Android.ACCOMPANIST_SYSTEM_UI_CONTROLLER)
43 | api(Lib.Android.LANDSCAPIST_GLIDE)
44 | api(Lib.Android.LANDSCAPIST_PLACEHOLDER)
45 | api(Lib.Android.COMPOSE_MATERIAL)
46 | api(Lib.Android.COMPOSE_TOOLING)
47 | debugApi(Lib.Android.COMPOSE_DEBUG_TOOLING)
48 | api(Lib.Android.ACTIVITY_COMPOSE)
49 | api(Lib.Android.COMPOSE_ICON)
50 |
51 | /* Dependency Injection */
52 | api(Lib.Di.hiltAndroid)
53 | ksp(Lib.Di.hiltAndroidCompiler)
54 | }
--------------------------------------------------------------------------------
/commonui/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/commonui/src/main/java/io/getstream/slackclone/commonui/keyboard/Keyboard.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.commonui.keyboard
2 |
3 | import android.graphics.Rect
4 | import android.view.ViewTreeObserver
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.DisposableEffect
7 | import androidx.compose.runtime.State
8 | import androidx.compose.runtime.mutableStateOf
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.ui.platform.LocalView
11 |
12 | sealed class Keyboard {
13 | data class Opened(var height: Int) : Keyboard()
14 | object Closed : Keyboard()
15 | }
16 |
17 | @Composable
18 | fun keyboardAsState(): State {
19 | val keyboardState = remember { mutableStateOf(Keyboard.Closed) }
20 | val view = LocalView.current
21 | DisposableEffect(view) {
22 | val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
23 | val rect = Rect()
24 | view.getWindowVisibleDisplayFrame(rect)
25 | val screenHeight = view.rootView.height
26 | val keypadHeight = screenHeight - rect.bottom
27 | keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
28 | Keyboard.Opened(screenHeight - keypadHeight)
29 | } else {
30 | Keyboard.Closed
31 | }
32 | }
33 | view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
34 |
35 | onDispose {
36 | view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
37 | }
38 | }
39 |
40 | return keyboardState
41 | }
42 |
--------------------------------------------------------------------------------
/commonui/src/main/java/io/getstream/slackclone/commonui/material/SlackSurfaceAppBar.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.commonui.material
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.foundation.layout.RowScope
5 | import androidx.compose.material.AppBarDefaults
6 | import androidx.compose.material.MaterialTheme
7 | import androidx.compose.material.TopAppBar
8 | import androidx.compose.material.contentColorFor
9 | import androidx.compose.material.primarySurface
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.unit.Dp
14 | import io.getstream.slackclone.commonui.theme.SlackCloneSurface
15 |
16 | @Composable
17 | fun SlackSurfaceAppBar(
18 | title: @Composable () -> Unit,
19 | modifier: Modifier = Modifier,
20 | navigationIcon: @Composable (() -> Unit)? = null,
21 | actions: @Composable RowScope.() -> Unit = {},
22 | backgroundColor: Color = MaterialTheme.colors.primarySurface,
23 | contentColor: Color = contentColorFor(backgroundColor),
24 | elevation: Dp = AppBarDefaults.TopAppBarElevation,
25 | ) {
26 | SlackCloneSurface(
27 | color = backgroundColor,
28 | contentColor = contentColor,
29 | elevation = elevation
30 | ) {
31 | TopAppBar(
32 | title, modifier, navigationIcon, actions, backgroundColor, contentColor, elevation
33 | )
34 | }
35 | }
36 |
37 | @Composable
38 | fun SlackSurfaceAppBar(
39 | modifier: Modifier = Modifier,
40 | backgroundColor: Color = MaterialTheme.colors.primarySurface,
41 | contentColor: Color = contentColorFor(backgroundColor),
42 | elevation: Dp = AppBarDefaults.TopAppBarElevation,
43 | contentPadding: PaddingValues = AppBarDefaults.ContentPadding,
44 | content: @Composable RowScope.() -> Unit
45 | ) {
46 | SlackCloneSurface(
47 | color = backgroundColor,
48 | contentColor = contentColor,
49 | elevation = elevation
50 | ) {
51 | TopAppBar(
52 | modifier, backgroundColor, contentColor, elevation, contentPadding, content
53 | )
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/commonui/src/main/java/io/getstream/slackclone/commonui/reusable/SlackImageBox.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.commonui.reusable
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.foundation.shape.CircleShape
8 | import androidx.compose.foundation.shape.RoundedCornerShape
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.draw.clip
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.res.painterResource
14 | import androidx.compose.ui.tooling.preview.Preview
15 | import androidx.compose.ui.unit.dp
16 | import androidx.constraintlayout.compose.ConstraintLayout
17 | import com.skydoves.landscapist.components.rememberImageComponent
18 | import com.skydoves.landscapist.glide.GlideImage
19 | import com.skydoves.landscapist.placeholder.shimmer.Shimmer
20 | import com.skydoves.landscapist.placeholder.shimmer.ShimmerPlugin
21 | import io.getstream.slackclone.commonui.R
22 | import io.getstream.slackclone.commonui.theme.ShimmerBackground
23 | import io.getstream.slackclone.commonui.theme.ShimmerHighLight
24 | import io.getstream.slackclone.commonui.theme.SlackCloneColorProvider
25 | import io.getstream.slackclone.commonui.theme.SlackCloneSurface
26 |
27 | @Composable
28 | fun SlackImageBox(modifier: Modifier, imageModel: Any) {
29 | GlideImage(
30 | imageModel = { imageModel },
31 | previewPlaceholder = painterResource(R.drawable.logo_stream),
32 | component = rememberImageComponent {
33 | // shows a shimmering effect when loading an image.
34 | +ShimmerPlugin(
35 | Shimmer.Flash(
36 | baseColor = ShimmerBackground,
37 | highlightColor = ShimmerHighLight,
38 | ),
39 | )
40 | },
41 | modifier = modifier.clip(RoundedCornerShape(8.dp))
42 | )
43 | }
44 |
45 | @Composable
46 | fun SlackOnlineBox(
47 | imageModel: Any,
48 | parentModifier: Modifier = Modifier.size(34.dp),
49 | imageModifier: Modifier = Modifier.size(28.dp)
50 | ) {
51 | ConstraintLayout(parentModifier) {
52 | val (image, indicator) = createRefs()
53 | SlackImageBox(
54 | imageModifier
55 | .constrainAs(image) {
56 | top.linkTo(parent.top)
57 | bottom.linkTo(parent.bottom)
58 | start.linkTo(parent.start)
59 | end.linkTo(parent.end)
60 | },
61 | imageModel
62 | )
63 | SlackCloneSurface(
64 | shape = CircleShape,
65 | border = BorderStroke(3.dp, color = SlackCloneColorProvider.colors.uiBackground),
66 | modifier = Modifier
67 | .constrainAs(indicator) {
68 | bottom.linkTo(parent.bottom)
69 | end.linkTo(parent.end)
70 | }
71 | .size(14.dp)
72 | ) {
73 | Box(
74 | modifier = Modifier
75 | .size(12.dp)
76 | .clip(CircleShape)
77 | .background(Color.Green)
78 | )
79 | }
80 | }
81 | }
82 |
83 | @Preview
84 | @Composable
85 | fun PrevSlackOnlineBox() {
86 | SlackOnlineBox(
87 | imageModel = R.drawable.logo_stream
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/commonui/src/main/java/io/getstream/slackclone/commonui/reusable/SlackListItem.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.commonui.reusable
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.material.Icon
8 | import androidx.compose.material.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.vector.ImageVector
13 | import androidx.compose.ui.unit.dp
14 | import io.getstream.slackclone.commonui.theme.SlackCloneColorProvider
15 | import io.getstream.slackclone.commonui.theme.SlackCloneTypography
16 |
17 | @Composable
18 | fun SlackListItem(
19 | icon: ImageVector,
20 | title: String,
21 | trailingItem: ImageVector? = null,
22 | onItemClick: () -> Unit = {}
23 | ) {
24 | Row(
25 | modifier = Modifier
26 | .padding(8.dp)
27 | .clickable {
28 | onItemClick()
29 | },
30 | verticalAlignment = Alignment.CenterVertically
31 | ) {
32 | Icon(
33 | imageVector = icon,
34 | contentDescription = null,
35 | tint = SlackCloneColorProvider.colors.textPrimary.copy(alpha = 0.4f),
36 | modifier = Modifier
37 | .size(28.dp)
38 | .padding(4.dp)
39 | )
40 | Text(
41 | text = title,
42 | style = SlackCloneTypography.subtitle1.copy(
43 | color = SlackCloneColorProvider.colors.textPrimary.copy(
44 | alpha = 0.8f
45 | )
46 | ),
47 | modifier = Modifier
48 | .weight(1f)
49 | .padding(8.dp)
50 | )
51 | trailingItem?.let { safeIcon ->
52 | Icon(
53 | imageVector = safeIcon,
54 | contentDescription = null,
55 | tint = SlackCloneColorProvider.colors.textPrimary.copy(alpha = 0.4f),
56 | modifier = Modifier
57 | .size(24.dp)
58 | .padding(4.dp)
59 | )
60 | }
61 | }
62 | }
63 |
64 | @Composable
65 | fun SlackListItem(
66 | icon: @Composable () -> Unit,
67 | center: @Composable (Modifier) -> Unit,
68 | trailingItem: @Composable () -> Unit? = {},
69 | onItemClick: () -> Unit = {}
70 | ) {
71 | Row(
72 | modifier = Modifier
73 | .padding(8.dp)
74 | .clickable {
75 | onItemClick()
76 | },
77 | verticalAlignment = Alignment.CenterVertically
78 | ) {
79 | icon()
80 |
81 | center(
82 | Modifier
83 | .weight(1f)
84 | .padding(8.dp)
85 | )
86 |
87 | trailingItem()
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/commonui/src/main/java/io/getstream/slackclone/commonui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.commonui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val SlackCloneColor = Color(0xff411540)
6 | val DarkAppBarColor = Color(0xff1a1b1e)
7 | val DarkBackground = Color(0xff1b1d21)
8 | val FunctionalRed = Color(0xFF40343F)
9 | val FunctionalRedDark = Color(0xFF40343F)
10 | val SlackLogoYellow = Color(0xffECB22E)
11 | val LineColorLight = Color.Black.copy(alpha = 0.4f)
12 | val LineColorDark = Color.White.copy(alpha = 0.3f)
13 | val ShimmerHighLight = Color(0xA3C2C2C2)
14 | val ShimmerBackground = Color(0xFF424242)
15 |
--------------------------------------------------------------------------------
/commonui/src/main/java/io/getstream/slackclone/commonui/theme/PraxisSurface.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.commonui.theme
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.border
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.material.LocalContentColor
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.CompositionLocalProvider
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.draw.clip
12 | import androidx.compose.ui.draw.shadow
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.graphics.RectangleShape
15 | import androidx.compose.ui.graphics.Shape
16 | import androidx.compose.ui.graphics.compositeOver
17 | import androidx.compose.ui.unit.Dp
18 | import androidx.compose.ui.unit.dp
19 | import androidx.compose.ui.zIndex
20 | import kotlin.math.ln
21 |
22 | /**
23 | * An alternative to [androidx.compose.material.Surface]
24 | */
25 | @Composable
26 | fun SlackCloneSurface(
27 | modifier: Modifier = Modifier,
28 | shape: Shape = RectangleShape,
29 | color: Color = SlackCloneColorProvider.colors.uiBackground,
30 | contentColor: Color = SlackCloneColorProvider.colors.textSecondary,
31 | border: BorderStroke? = null,
32 | elevation: Dp = 0.dp,
33 | content: @Composable () -> Unit
34 | ) {
35 | Box(
36 | modifier = modifier
37 | .shadow(elevation = elevation, shape = shape, clip = false)
38 | .zIndex(elevation.value)
39 | .then(if (border != null) Modifier.border(border, shape) else Modifier)
40 | .background(
41 | color = getBackgroundColorForElevation(color, elevation),
42 | shape = shape
43 | )
44 | .clip(shape)
45 | ) {
46 | CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
47 | }
48 | }
49 |
50 | @Composable
51 | private fun getBackgroundColorForElevation(
52 | color: Color,
53 | elevation: Dp
54 | ): Color {
55 | return if (elevation > 0.dp
56 | ) {
57 | color.withElevation(elevation)
58 | } else {
59 | color
60 | }
61 | }
62 |
63 | /**
64 | * Applies a [Color.White] overlay to this color based on the [elevation]. This increases visibility
65 | * of elevation for surfaces in a dark theme.
66 | */
67 | private fun Color.withElevation(elevation: Dp): Color {
68 | val foreground = calculateForeground(elevation)
69 | return foreground.compositeOver(this)
70 | }
71 |
72 | /**
73 | * @return the alpha-modified [Color.White] to overlay on top of the surface color to produce
74 | * the resultant color.
75 | */
76 | private fun calculateForeground(elevation: Dp): Color {
77 | val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 20f
78 | return Color.White.copy(alpha = alpha)
79 | }
80 |
--------------------------------------------------------------------------------
/commonui/src/main/java/io/getstream/slackclone/commonui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.commonui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val SlackCloneShapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(6.dp),
10 | large = RoundedCornerShape(10.dp)
11 | )
12 |
--------------------------------------------------------------------------------
/commonui/src/main/java/io/getstream/slackclone/commonui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package io.getstream.slackclone.commonui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.Font
6 | import androidx.compose.ui.text.font.FontFamily
7 | import androidx.compose.ui.text.font.FontWeight
8 | import androidx.compose.ui.unit.sp
9 | import io.getstream.slackclone.commonui.R
10 |
11 | // Set of Material typography styles to start with
12 |
13 | val slackFontFamily =
14 | FontFamily(
15 | Font(R.font.lato_bold, weight = FontWeight.Bold),
16 | Font(R.font.lato_light, weight = FontWeight.Light),
17 | Font(R.font.lato_regular)
18 | )
19 |
20 | val SlackCloneTypography = Typography(
21 | defaultFontFamily = slackFontFamily,
22 | body1 = TextStyle(
23 | fontFamily = slackFontFamily,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 16.sp
26 | ),
27 | button = TextStyle(
28 | fontFamily = slackFontFamily,
29 | fontWeight = FontWeight.W500,
30 | fontSize = 14.sp
31 | ),
32 | caption = TextStyle(
33 | fontFamily = slackFontFamily,
34 | fontWeight = FontWeight.Normal,
35 | fontSize = 12.sp
36 | )
37 | )
38 |
--------------------------------------------------------------------------------
/commonui/src/main/res/anim/slide_left_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/commonui/src/main/res/anim/slide_left_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/commonui/src/main/res/anim/slide_right_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/commonui/src/main/res/anim/slide_right_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/commonui/src/main/res/drawable/ic_email.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/commonui/src/main/res/drawable/ic_eye.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/commonui/src/main/res/drawable/logo_compose.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/commonui/src/main/res/drawable/logo_compose.png
--------------------------------------------------------------------------------
/commonui/src/main/res/drawable/logo_flutter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/commonui/src/main/res/drawable/logo_flutter.png
--------------------------------------------------------------------------------
/commonui/src/main/res/drawable/logo_gdg.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/commonui/src/main/res/drawable/logo_gdg.jpeg
--------------------------------------------------------------------------------
/commonui/src/main/res/drawable/logo_kotlin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/commonui/src/main/res/drawable/logo_kotlin.png
--------------------------------------------------------------------------------
/commonui/src/main/res/drawable/logo_stream.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/commonui/src/main/res/drawable/logo_stream.png
--------------------------------------------------------------------------------
/commonui/src/main/res/font/lato_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/commonui/src/main/res/font/lato_bold.ttf
--------------------------------------------------------------------------------
/commonui/src/main/res/font/lato_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/commonui/src/main/res/font/lato_light.ttf
--------------------------------------------------------------------------------
/commonui/src/main/res/font/lato_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/commonui/src/main/res/font/lato_regular.ttf
--------------------------------------------------------------------------------
/commonui/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/commonui/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/commonui/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/commonui/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/commonui/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/commonui/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/commonui/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/commonui/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/commonui/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/stream-slack-clone-android/0769665eaf2ae7d64797712368d82a8e13f90708/commonui/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/commonui/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------