diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index fc17b63e..56dfabb5 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -8,15 +8,6 @@
-
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 61a9130c..fb7f4a8a 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index be06a049..d6088b85 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,6 +14,7 @@ buildscript {
classpath Dependencies.kotlinGradlePlugin
classpath Dependencies.navigationSafeArgsGradlePlugin
classpath Dependencies.ktlintPlugin
+ classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20'
}
}
diff --git a/settings.gradle b/settings.gradle
index 1bf8ba77..919a07ba 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -5,3 +5,4 @@ include(
)
+include ':slack-clone-sample'
diff --git a/slack-clone-sample/.gitignore b/slack-clone-sample/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/slack-clone-sample/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/slack-clone-sample/build.gradle b/slack-clone-sample/build.gradle
new file mode 100644
index 00000000..0ac6770b
--- /dev/null
+++ b/slack-clone-sample/build.gradle
@@ -0,0 +1,54 @@
+import io.getstream.samples.Dependencies
+
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+
+android {
+ compileSdkVersion 30
+
+ defaultConfig {
+ applicationId "io.getstream.slackclonesample"
+ minSdkVersion 21
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ viewBinding {
+ enabled true
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+ implementation Dependencies.kotlinStdLib
+ implementation Dependencies.androidxAppCompat
+ implementation Dependencies.androidxCoreKtx
+ implementation Dependencies.androidxConstraintLayout
+ implementation Dependencies.materialComponents
+ implementation Dependencies.androidxLifecycleExtensions
+ implementation Dependencies.fragmentKtx
+ implementation Dependencies.androidxActivityKtx
+
+ implementation 'io.getstream:stream-chat-android-ui-components:4.14.2'
+
+ testImplementation 'junit:junit:4.+'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+}
\ No newline at end of file
diff --git a/slack-clone-sample/proguard-rules.pro b/slack-clone-sample/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/slack-clone-sample/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/AndroidManifest.xml b/slack-clone-sample/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..dbfb7078
--- /dev/null
+++ b/slack-clone-sample/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/AppConfig.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/AppConfig.kt
new file mode 100644
index 00000000..565a4e28
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/AppConfig.kt
@@ -0,0 +1,125 @@
+package io.getstream.slackclonesample
+
+import io.getstream.slackclonesample.domain.user.SampleUser
+
+object AppConfig {
+ const val apiKey: String = "qx5us2v6xvmh"
+ const val apiUrl: String = "chat-us-east-staging.stream-io-api.com"
+ const val apiTimeout: Int = 6000
+ const val cndTimeout: Int = 30000
+
+ val availableUsers: List = listOf(
+ SampleUser(
+ apiKey = apiKey,
+ name = "Jc",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FJc.png?alt=media",
+ id = "1f37e58d-d8b0-476a-a4f2-f8611e0d85d9",
+ token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMWYzN2U1OGQtZDhiMC00NzZhLWE0ZjItZjg2MTFlMGQ4NWQ5In0.l3u9P1NKhJ91rI1tzOcABGh045Kj69-iVkC2yUtohVw"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Fra",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FFra.png?alt=media",
+ id = "Fra",
+ token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiRnJhIn0.ENQGHEsAL3WjVhd_qTiJa_9ojGKi2ftJ8xlocT8SVX4"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Carter",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FCarter.png?alt=media",
+ id = "6d95273b-33f0-40f5-b07c-0da261092074",
+ token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNmQ5NTI3M2ItMzNmMC00MGY1LWIwN2MtMGRhMjYxMDkyMDc0In0.lT5O4EmWzhRKPTau6dHP4F6M42EA2aN_8-iAPuiFPLc"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Dmitrii",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FDmitrii.png?alt=media",
+ id = "1e330111-670d-49a7-8f08-e6734338c641",
+ token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMWUzMzAxMTEtNjcwZC00OWE3LThmMDgtZTY3MzQzMzhjNjQxIn0.YEFdEMWj5rurQKr0QMrvO72jGZHU-AlpUIbyY4jxYdU"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Leandro",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FLeandro.png?alt=media",
+ id = "29e46def-88f4-4b6a-a10c-584d10c4fdc9",
+ token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMjllNDZkZWYtODhmNC00YjZhLWExMGMtNTg0ZDEwYzRmZGM5In0.Mxr4Prnb1-EVM5NSSP2EugLApSChoKnVFwe7ZO15V_U"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Marton",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FMarton.png?alt=media",
+ id = "1f052c08-f682-4a83-896c-9f19a68bd2bb",
+ token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMWYwNTJjMDgtZjY4Mi00YTgzLTg5NmMtOWYxOWE2OGJkMmJiIn0.L-cQ-DYubOzFpsg94OEwlTRYjat9G4cqfAgzBPALW0g"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Oleg",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FOleg.png?alt=media",
+ id = "0d3e6e63-6200-4dd1-a841-4050664891e2",
+ token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMGQzZTZlNjMtNjIwMC00ZGQxLWE4NDEtNDA1MDY2NDg5MWUyIn0.osFIgnle17f6yEkK7rPJguQaKhOiawAO3BylYaiRTqE"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Rafal",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FRafal.png?alt=media",
+ id = "12fb0ed9-93d8-48a5-9885-28e41f2e4c43",
+ token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMTJmYjBlZDktOTNkOC00OGE1LTk4ODUtMjhlNDFmMmU0YzQzIn0.t_oc_DEwTav7ni0z4bi8Xla_5Zj5cI6l3rKxwoCvtB0"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Samuel",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FSam.png?alt=media",
+ id = "5531a8cb-3b81-4a54-b424-7ae4e27bf8ba",
+ token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNTUzMWE4Y2ItM2I4MS00YTU0LWI0MjQtN2FlNGUyN2JmOGJhIn0.PXkmukg3JU4igH_YUMr7WC7a1EcwKBr_C5V2ouBlmIs"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Tommaso",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FTommaso.png?alt=media",
+ id = "06356564-149f-4b2c-8525-d22056fec404",
+ token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMDYzNTY1NjQtMTQ5Zi00YjJjLTg1MjUtZDIyMDU2ZmVjNDA0In0.R3-HY9Cno62yIhCjLXDBR8LF7y1udwX8m4LLNP2dIZo"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Thierry",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FThierry.png?alt=media",
+ id = "ad7d9314-5071-4d61-98a1-ffa643ce824a",
+ token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiYWQ3ZDkzMTQtNTA3MS00ZDYxLTk4YTEtZmZhNjQzY2U4MjRhIn0.iF4UWGFtX0eTAIBTCum7fjD_TKn8wjEqb3PVxJrwbuM"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Zetra",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FZetra.png?alt=media",
+ id = "cebf562a-4806-4c64-a827-59d50aac42ba",
+ token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY2ViZjU2MmEtNDgwNi00YzY0LWE4MjctNTlkNTBhYWM0MmJhIn0.kuXab7RhQRHdsErEW5tTN_mmuyLPNU4ZbprvuPXM4OY"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Qatest0",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FQatest0.png?alt=media",
+ id = "qatest0",
+ token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MCJ9.Vow00KvvhLvWRZIPKomXQOYpBL_P-_-eDeDKmBRvEj4"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Qatest1",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FQatest1.png?alt=media",
+ id = "qatest1",
+ token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MSJ9.H1nlYibjgp1HfaOd0sA_T4038tjsN61mJWxvUjmRQI0"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Qatest2",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FQatest2.png?alt=media",
+ id = "qatest2",
+ token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MiJ9.GYp9ikLtU2eG9Mq7tmHThzbV7C8W82j18sExuO7-ogc"
+ ),
+ SampleUser(
+ apiKey = apiKey,
+ name = "Qatest3",
+ image = "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FQatest3.png?alt=media",
+ id = "qatest3",
+ token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MyJ9.kLZJz5kl7e3Zw7i2T39Yp05_nAmh9RGG0rt6-5zOpfE"
+ ),
+ )
+}
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/ChatInitializer.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/ChatInitializer.kt
new file mode 100644
index 00000000..e3e5c996
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/ChatInitializer.kt
@@ -0,0 +1,22 @@
+package io.getstream.slackclonesample
+
+import android.content.Context
+import io.getstream.chat.android.client.ChatClient
+import io.getstream.chat.android.client.logger.ChatLogLevel
+import io.getstream.chat.android.livedata.ChatDomain
+
+class ChatInitializer(private val context: Context) {
+
+ @Suppress("UNUSED_VARIABLE")
+ fun init(apiKey: String) {
+ val logLevel = if (BuildConfig.DEBUG) ChatLogLevel.ALL else ChatLogLevel.NOTHING
+ val client = ChatClient.Builder(apiKey, context)
+ .logLevel(logLevel)
+ .build()
+
+ val domain = ChatDomain.Builder(client, context)
+ .userPresenceEnabled()
+ .offlineEnabled()
+ .build()
+ }
+}
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/SlackSampleApp.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/SlackSampleApp.kt
new file mode 100644
index 00000000..82171c36
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/SlackSampleApp.kt
@@ -0,0 +1,31 @@
+package io.getstream.slackclonesample
+
+import android.app.Application
+import io.getstream.slackclonesample.data.UserRepository
+import io.getstream.slackclonesample.domain.user.SampleUser
+
+class SlackSampleApp: Application() {
+
+ val userRepository = UserRepository(this)
+ val chatInitializer = ChatInitializer(this)
+
+ override fun onCreate() {
+ super.onCreate()
+ chatInitializer.init(getApiKey())
+ instance = this
+ }
+
+ private fun getApiKey(): String {
+ val user = userRepository.getUser()
+ return if (user != SampleUser.None) {
+ user.apiKey
+ } else {
+ AppConfig.apiKey
+ }
+ }
+
+ companion object {
+ lateinit var instance: SlackSampleApp
+ private set
+ }
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/data/UserRepository.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/data/UserRepository.kt
new file mode 100644
index 00000000..7524c7ea
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/data/UserRepository.kt
@@ -0,0 +1,48 @@
+package io.getstream.slackclonesample.data
+
+import android.content.Context
+import android.content.SharedPreferences
+import io.getstream.slackclonesample.domain.user.SampleUser
+
+class UserRepository(context: Context) {
+
+ private val prefs: SharedPreferences by lazy {
+ context.getSharedPreferences(USER_PREFS_NAME, Context.MODE_PRIVATE)
+ }
+
+ fun getUser(): SampleUser {
+ val apiKey = prefs.getString(KEY_API_KEY, null)
+ val id = prefs.getString(KEY_ID, null)
+ val name = prefs.getString(KEY_NAME, null)
+ val token = prefs.getString(KEY_TOKEN, null)
+ val image = prefs.getString(KEY_IMAGE, null)
+ return if (apiKey != null && id != null && name != null && token != null && image != null) {
+ SampleUser(apiKey = apiKey, id = id, name = name, token = token, image = image)
+ } else {
+ SampleUser.None
+ }
+ }
+
+ fun setUser(user: SampleUser) {
+ prefs.edit()
+ .putString(KEY_API_KEY, user.apiKey)
+ .putString(KEY_ID, user.id)
+ .putString(KEY_NAME, user.name)
+ .putString(KEY_TOKEN, user.token)
+ .putString(KEY_IMAGE, user.image)
+ .apply()
+ }
+
+ fun clearUser() {
+ prefs.edit().clear().apply()
+ }
+
+ private companion object {
+ private const val USER_PREFS_NAME = "logged_in_user"
+ private const val KEY_API_KEY = "api_key"
+ private const val KEY_ID = "id"
+ private const val KEY_NAME = "name"
+ private const val KEY_TOKEN = "token"
+ private const val KEY_IMAGE = "image"
+ }
+}
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/domain/user/SampleUser.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/domain/user/SampleUser.kt
new file mode 100644
index 00000000..be3074c7
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/domain/user/SampleUser.kt
@@ -0,0 +1,14 @@
+package io.getstream.slackclonesample.domain.user
+
+data class SampleUser(
+ val apiKey: String,
+ val id: String,
+ val name: String,
+ val token: String,
+ val image: String,
+) {
+
+ companion object {
+ val None: SampleUser = SampleUser("", "", "", "", "https://getstream.io/random_png?id=none&name=none&size=200")
+ }
+}
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/ChatFragment.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/ChatFragment.kt
new file mode 100644
index 00000000..f2a6bb99
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/ChatFragment.kt
@@ -0,0 +1,202 @@
+package io.getstream.slackclonesample.feature.chat
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import com.getstream.sdk.chat.viewmodel.MessageInputViewModel
+import com.getstream.sdk.chat.viewmodel.messages.MessageListViewModel
+import com.getstream.sdk.chat.viewmodel.messages.getCreatedAtOrThrow
+import io.getstream.chat.android.client.errors.ChatNetworkError
+import io.getstream.chat.android.client.models.Flag
+import io.getstream.chat.android.client.models.Message
+import io.getstream.chat.android.client.utils.Result
+import io.getstream.chat.android.ui.message.list.header.viewmodel.MessageListHeaderViewModel
+import io.getstream.chat.android.ui.message.list.viewmodel.bindView
+import io.getstream.chat.android.ui.message.list.viewmodel.factory.MessageListViewModelFactory
+import io.getstream.slackclonesample.databinding.FragmentChatBinding
+import io.getstream.slackclonesample.feature.chat.viewholder.SlackMessageViewHolderFactory
+import io.getstream.slackclonesample.feature.home.HomeViewModel
+import io.getstream.slackclonesample.util.useAdjustResize
+import kotlinx.coroutines.flow.collect
+import java.util.*
+
+class ChatFragment : Fragment() {
+
+ private val cid: String by lazy {
+ requireArguments().getString("cid")
+ ?: throw IllegalArgumentException("cid argument can't be null")
+ }
+ private val factory: MessageListViewModelFactory by lazy { MessageListViewModelFactory(cid = cid) }
+ private val headerViewModel: MessageListHeaderViewModel by viewModels { factory }
+ private val messageListViewModel: MessageListViewModel by viewModels { factory }
+ private val messageInputViewModel: MessageInputViewModel by viewModels { factory }
+
+ private val homeViewModel: HomeViewModel by activityViewModels()
+
+ private var _binding: FragmentChatBinding? = null
+ private val binding get() = _binding!!
+
+ private val maxAttachmentFile: Int = 20
+ private var messageListSubtitle: String? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launchWhenStarted {
+ homeViewModel.state.collect(::renderState)
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ _binding = FragmentChatBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.messageListHeaderView.root.setNavigationOnClickListener {
+ homeViewModel.submitAction(HomeViewModel.Action.OpenDrawerAction)
+ }
+ initMessagesViewModel()
+ configureMessageInputView()
+ initMessageInputViewModel()
+ configureBackButtonHandling()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ private fun configureMessageInputView() {
+ lifecycleScope.launchWhenCreated {
+ /*binding.messageInputView.listenForBigAttachments(
+ object : MessageInputView.BigFileSelectionListener {
+ override fun handleBigFileSelected(hasBigFile: Boolean) {
+ *//*if (hasBigFile) {
+ messageListSubtitle = binding.messagesHeaderView.getOnlineStateSubtitle()
+ binding.messagesHeaderView.setOnlineStateSubtitle(
+ requireContext().getString(
+ R.string.chat_fragment_big_attachment_subtitle,
+ maxAttachmentFile
+ )
+ )
+ } else {
+ messageListSubtitle?.let(binding.messagesHeaderView::setOnlineStateSubtitle)
+ }*//*
+ }
+ }
+ )*/
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ useAdjustResize()
+ }
+
+ private fun renderState(state: HomeViewModel.State) {
+ state.userId?.let {
+ headerViewModel.bindView(
+ binding.messageListHeaderView.root, viewLifecycleOwner,
+ state.userId
+ )
+ }
+ }
+
+ private fun configureBackButtonHandling() {
+ activity?.apply {
+ onBackPressedDispatcher.addCallback(
+ viewLifecycleOwner,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed)
+ }
+ }
+ )
+ }
+ /*binding.messagesHeaderView.setBackButtonClickListener {
+ messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed)
+ }*/
+ }
+
+ private fun initMessageInputViewModel() {
+ messageInputViewModel.apply {
+ bindView(binding.messageInputView, viewLifecycleOwner)
+ messageListViewModel.mode.observe(viewLifecycleOwner) {
+ when (it) {
+ is MessageListViewModel.Mode.Thread -> {
+ headerViewModel.setActiveThread(it.parentMessage)
+ messageInputViewModel.setActiveThread(it.parentMessage)
+ }
+ is MessageListViewModel.Mode.Normal -> {
+ headerViewModel.resetThread()
+ messageInputViewModel.resetThread()
+ }
+ }
+ }
+ binding.messageListView.setMessageEditHandler(::postMessageToEdit)
+ }
+ }
+
+ private fun initMessagesViewModel() {
+ val calendar = Calendar.getInstance()
+ binding.messageListView.setMessageViewHolderFactory(SlackMessageViewHolderFactory())
+ messageListViewModel.apply {
+ bindView(binding.messageListView, viewLifecycleOwner)
+ setDateSeparatorHandler { previousMessage, message ->
+ if (previousMessage == null) {
+ true
+ } else {
+ shouldShowDateSeparator(calendar, previousMessage, message)
+ }
+ }
+ setThreadDateSeparatorHandler { previousMessage, message ->
+ if (previousMessage == null) {
+ false
+ } else {
+ shouldShowDateSeparator(calendar, previousMessage, message)
+ }
+ }
+ state.observe(viewLifecycleOwner) {
+ when (it) {
+ is MessageListViewModel.State.Loading -> Unit
+ is MessageListViewModel.State.Result -> Unit
+ }
+ }
+ }
+ }
+
+ private fun shouldShowDateSeparator(
+ calendar: Calendar,
+ previousMessage: Message,
+ message: Message
+ ): Boolean {
+ val (previousYear, previousDayOfYear) = calendar.run {
+ time = previousMessage.getCreatedAtOrThrow()
+ get(Calendar.YEAR) to get(Calendar.DAY_OF_YEAR)
+ }
+ val (year, dayOfYear) = calendar.run {
+ time = message.getCreatedAtOrThrow()
+ get(Calendar.YEAR) to get(Calendar.DAY_OF_YEAR)
+ }
+ return previousYear != year || previousDayOfYear != dayOfYear
+ }
+
+ private fun Result.isAlreadyExistsError(): Boolean {
+ if (!isError) {
+ return false
+ }
+ val chatError = error() as ChatNetworkError
+ return chatError.streamCode == 4
+ }
+}
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/CustomMessageInputViewBinding.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/CustomMessageInputViewBinding.kt
new file mode 100644
index 00000000..3eeb44ea
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/CustomMessageInputViewBinding.kt
@@ -0,0 +1,41 @@
+package io.getstream.slackclonesample.feature.chat
+
+import android.text.Editable
+import android.text.TextWatcher
+import androidx.lifecycle.LifecycleOwner
+import com.getstream.sdk.chat.viewmodel.MessageInputViewModel
+import io.getstream.chat.android.client.models.Member
+import io.getstream.chat.android.ui.message.input.MessageInputView
+import io.getstream.slackclonesample.databinding.CustomMessageInputBinding
+
+@JvmName("bind")
+public fun MessageInputViewModel.bindView(
+ bindingView: CustomMessageInputBinding,
+ lifecycleOwner: LifecycleOwner
+) {
+ val handler = MessageInputView.DefaultUserLookupHandler(emptyList())
+ members.observe(lifecycleOwner) { members ->
+ handler.users = members.map(Member::user)
+ }
+
+ bindingView.sendMessage.setOnClickListener {
+ this.sendMessage(bindingView.messageInput.text.toString())
+ bindingView.messageInput.setText("")
+ }
+
+ // listen to typing events and connect to the view model
+ bindingView.messageInput.addTextChangedListener(TypingTextWatcher(this))
+}
+
+class TypingTextWatcher(private val viewModel: MessageInputViewModel) : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
+
+ override fun afterTextChanged(s: Editable?) {
+ if (s.toString().isNotEmpty()) {
+ viewModel.keystroke()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/CustomMessageListHeaderViewBinding.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/CustomMessageListHeaderViewBinding.kt
new file mode 100644
index 00000000..bec142a0
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/CustomMessageListHeaderViewBinding.kt
@@ -0,0 +1,27 @@
+package io.getstream.slackclonesample.feature.chat
+
+import android.util.Log
+import androidx.lifecycle.LifecycleOwner
+import com.google.android.material.appbar.MaterialToolbar
+import io.getstream.chat.android.client.models.name
+import io.getstream.chat.android.ui.common.extensions.getDisplayName
+import io.getstream.chat.android.ui.message.list.header.viewmodel.MessageListHeaderViewModel
+
+@JvmName("bind")
+public fun MessageListHeaderViewModel.bindView(
+ view: MaterialToolbar,
+ lifecycle: LifecycleOwner,
+ currentUserId: String
+) {
+ channelState.observe(lifecycle) {
+ Log.d("Channel Member Count", "${it.memberCount}")
+ if (it.memberCount == 2) {
+ val otherUserName =
+ if (it.members[0].getUserId() == currentUserId) it.members[1].user.name
+ else it.members[0].user.name
+ view.title = otherUserName
+ } else {
+ view.title = "# ${it.getDisplayName(view.context).replace(' ', '_').lowercase()}"
+ }
+ }
+}
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/viewholder/SlackDateDividerViewHolder.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/viewholder/SlackDateDividerViewHolder.kt
new file mode 100644
index 00000000..dcd143b5
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/viewholder/SlackDateDividerViewHolder.kt
@@ -0,0 +1,36 @@
+package io.getstream.slackclonesample.feature.chat.viewholder
+
+import android.text.format.DateUtils
+import android.view.ContextThemeWrapper
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.getstream.sdk.chat.adapter.MessageListItem
+import io.getstream.chat.android.ui.R
+import io.getstream.chat.android.ui.message.list.adapter.BaseMessageItemViewHolder
+import io.getstream.chat.android.ui.message.list.adapter.MessageListItemPayloadDiff
+import io.getstream.slackclonesample.databinding.ItemDateDividerBinding
+
+class SlackDateDividerViewHolder(
+ private val parentView: ViewGroup,
+ private val binding: ItemDateDividerBinding = ItemDateDividerBinding.inflate(
+ LayoutInflater.from(
+ ContextThemeWrapper(parentView.context, R.style.StreamUiTheme)
+ ),
+ parentView,
+ false
+ ),
+) : BaseMessageItemViewHolder(binding.root) {
+
+ override fun bindData(data: MessageListItem.DateSeparatorItem, diff: MessageListItemPayloadDiff?) {
+ with(binding) {
+ dateLabel.text =
+ DateUtils.getRelativeTimeSpanString(
+ data.date.time,
+ System.currentTimeMillis(),
+ DateUtils.DAY_IN_MILLIS,
+ DateUtils.FORMAT_ABBREV_RELATIVE
+ )
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/viewholder/SlackGiphyViewHolder.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/viewholder/SlackGiphyViewHolder.kt
new file mode 100644
index 00000000..31cfeeff
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/viewholder/SlackGiphyViewHolder.kt
@@ -0,0 +1,56 @@
+package io.getstream.slackclonesample.feature.chat.viewholder
+
+import android.content.res.Resources
+import android.view.ContextThemeWrapper
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.getstream.sdk.chat.adapter.MessageListItem
+import io.getstream.chat.android.ui.R
+import io.getstream.chat.android.ui.message.list.adapter.BaseMessageItemViewHolder
+import io.getstream.chat.android.ui.message.list.adapter.MessageListItemPayloadDiff
+import io.getstream.slackclonesample.databinding.ItemMessageGiphyBinding
+
+class SlackGiphyViewHolder(
+ private val parentView: ViewGroup,
+ private val binding: ItemMessageGiphyBinding = ItemMessageGiphyBinding.inflate(
+ LayoutInflater.from(
+ ContextThemeWrapper(parentView.context, R.style.StreamUiTheme)
+ ),
+ parentView,
+ false
+ ),
+) : BaseMessageItemViewHolder(binding.root) {
+
+ private val resources: Resources = parentView.context.resources
+
+ override fun bindData(data: MessageListItem.MessageItem, diff: MessageListItemPayloadDiff?) {
+ applyStyle()
+ /* data.message
+ .attachments
+ .firstOrNull()
+ ?.let(binding.mediaAttachmentView::showAttachment)*/
+
+ binding.giphyQueryTextView.text = data.message
+ .text
+ .replace(GIPHY_PREFIX, "")
+ }
+
+ private fun applyStyle() {
+
+ // Apply default style
+ binding.apply {
+ cardView.setCardBackgroundColor(resources.getColor(R.color.stream_ui_white))
+ cardView.elevation = resources.getDimension(R.dimen.stream_ui_elevation_small)
+
+ horizontalDivider.setBackgroundColor(resources.getColor(R.color.stream_ui_border))
+ verticalDivider1.setBackgroundColor(resources.getColor(R.color.stream_ui_border))
+ verticalDivider2.setBackgroundColor(resources.getColor(R.color.stream_ui_border))
+
+ giphyIconImageView.setImageDrawable(resources.getDrawable(R.drawable.stream_ui_ic_giphy))
+ }
+ }
+
+ private companion object {
+ private const val GIPHY_PREFIX = "/giphy "
+ }
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/viewholder/SlackMessageViewHolderFactory.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/viewholder/SlackMessageViewHolderFactory.kt
new file mode 100644
index 00000000..c160645d
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/viewholder/SlackMessageViewHolderFactory.kt
@@ -0,0 +1,29 @@
+package io.getstream.slackclonesample.feature.chat.viewholder
+
+import android.view.ViewGroup
+import com.getstream.sdk.chat.adapter.MessageListItem
+import io.getstream.chat.android.ui.message.list.adapter.BaseMessageItemViewHolder
+import io.getstream.chat.android.ui.message.list.adapter.MessageListItemViewHolderFactory
+import io.getstream.chat.android.ui.message.list.adapter.MessageListItemViewType
+
+class SlackMessageViewHolderFactory : MessageListItemViewHolderFactory() {
+ override fun createViewHolder(
+ parentView: ViewGroup,
+ viewType: Int,
+ ): BaseMessageItemViewHolder {
+ return when (viewType) {
+ MessageListItemViewType.PLAIN_TEXT -> {
+ SlackPlainTextViewHolder(parentView)
+ }
+ MessageListItemViewType.DATE_DIVIDER -> {
+ SlackDateDividerViewHolder(parentView)
+ }
+ MessageListItemViewType.GIPHY -> {
+ SlackGiphyViewHolder(parentView)
+ }
+ else -> {
+ super.createViewHolder(parentView, viewType)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/viewholder/SlackPlainTextViewHolder.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/viewholder/SlackPlainTextViewHolder.kt
new file mode 100644
index 00000000..2d6aaac6
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/chat/viewholder/SlackPlainTextViewHolder.kt
@@ -0,0 +1,61 @@
+package io.getstream.slackclonesample.feature.chat.viewholder
+
+import android.graphics.Color
+import android.view.ContextThemeWrapper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
+import com.getstream.sdk.chat.adapter.MessageListItem
+import io.getstream.chat.android.client.models.name
+import io.getstream.chat.android.ui.ChatUI
+import io.getstream.chat.android.ui.R
+import io.getstream.chat.android.ui.common.markdown.ChatMarkdown
+import io.getstream.chat.android.ui.message.list.adapter.BaseMessageItemViewHolder
+import io.getstream.chat.android.ui.message.list.adapter.MessageListItemPayloadDiff
+import io.getstream.slackclonesample.databinding.ItemMessageListBinding
+
+class SlackPlainTextViewHolder(
+ private val parentView: ViewGroup,
+ private val binding: ItemMessageListBinding = ItemMessageListBinding.inflate(
+ LayoutInflater.from(
+ ContextThemeWrapper(parentView.context, R.style.StreamUiTheme)
+ ),
+ parentView,
+ false
+ ),
+) : BaseMessageItemViewHolder(binding.root) {
+
+ private val markdown: ChatMarkdown by lazy { ChatUI.markdown }
+
+ override fun bindData(data: MessageListItem.MessageItem, diff: MessageListItemPayloadDiff?) {
+ with(binding) {
+ markdown.setText(messageText, data.message.text)
+ decoratePlainText(data, messageText)
+
+ if (MessageListItem.Position.TOP in data.positions) {
+ avatarView.isVisible = true
+ avatarView.setUserData(data.message.user)
+ } else {
+ avatarView.isVisible = false
+ }
+
+ if (MessageListItem.Position.TOP in data.positions) {
+ userNameTextView.text = data.message.user.name
+ } else {
+ userNameTextView.visibility = View.GONE
+ messageText.updateLayoutParams {
+ this.marginStart = parentView.context.resources.getDimensionPixelSize(R.dimen.stream_ui_avatar_size_medium)
+ }
+ }
+
+ }
+ }
+
+ private fun decoratePlainText(data: MessageListItem.MessageItem, messageText: TextView) {
+ messageText.setTextColor(Color.BLACK)
+ }
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/CustomChannelViewHolder.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/CustomChannelViewHolder.kt
new file mode 100644
index 00000000..7ef2639d
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/CustomChannelViewHolder.kt
@@ -0,0 +1,55 @@
+package io.getstream.slackclonesample.feature.home
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import io.getstream.chat.android.client.models.Channel
+import io.getstream.chat.android.ui.channel.list.ChannelListView
+import io.getstream.chat.android.ui.channel.list.adapter.ChannelListItem
+import io.getstream.chat.android.ui.channel.list.adapter.ChannelListPayloadDiff
+import io.getstream.chat.android.ui.channel.list.adapter.viewholder.BaseChannelListItemViewHolder
+import io.getstream.chat.android.ui.channel.list.adapter.viewholder.ChannelListItemViewHolderFactory
+import io.getstream.chat.android.ui.common.extensions.getDisplayName
+import io.getstream.slackclonesample.databinding.CustomChannelListItemBinding
+
+class CustomChannelListItemViewHolderFactory : ChannelListItemViewHolderFactory() {
+
+ override fun getItemViewType(item: ChannelListItem): Int {
+ return super.getItemViewType(item)
+ }
+
+ override fun createViewHolder(
+ parentView: ViewGroup,
+ viewType: Int
+ ): BaseChannelListItemViewHolder {
+ return super.createViewHolder(parentView, viewType)
+ }
+
+ override fun createChannelViewHolder(parentView: ViewGroup): BaseChannelListItemViewHolder {
+ return CustomChannelListItemViewHolder(parentView, listenerContainer.channelClickListener)
+ }
+}
+
+class CustomChannelListItemViewHolder(
+ parent: ViewGroup,
+ private val channelClickListener: ChannelListView.ChannelClickListener,
+ private val binding: CustomChannelListItemBinding = CustomChannelListItemBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ ),
+) : BaseChannelListItemViewHolder(binding.root) {
+
+ private lateinit var channel: Channel
+
+ init {
+ binding.root.setOnClickListener { channelClickListener.onClick(channel) }
+ }
+
+ override fun bind(channel: Channel, diff: ChannelListPayloadDiff) {
+ this.channel = channel
+
+ binding.apply {
+ nameTextView.text = "# ${channel.getDisplayName(itemView.context)}"
+ }
+ }
+}
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/HomeActivity.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/HomeActivity.kt
new file mode 100644
index 00000000..1aa257a6
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/HomeActivity.kt
@@ -0,0 +1,165 @@
+package io.getstream.slackclonesample.feature.home
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.view.View
+import android.widget.FrameLayout
+import androidx.activity.viewModels
+import androidx.core.os.bundleOf
+import androidx.core.view.GravityCompat
+import androidx.core.view.doOnLayout
+import androidx.core.view.updatePadding
+import androidx.drawerlayout.widget.DrawerLayout
+import androidx.fragment.app.commit
+import androidx.fragment.app.replace
+import androidx.lifecycle.lifecycleScope
+import io.getstream.slackclonesample.AppConfig
+import io.getstream.slackclonesample.SlackSampleApp
+import io.getstream.slackclonesample.feature.chat.ChatFragment
+import io.getstream.slackclonesample.util.viewModelProviderFactoryOf
+import io.getstream.chat.android.client.ChatClient
+import io.getstream.chat.android.client.logger.ChatLogger
+import io.getstream.chat.android.client.models.User
+import io.getstream.chat.android.client.models.image
+import io.getstream.chat.android.client.models.name
+import io.getstream.chat.android.ui.channel.list.header.ChannelListHeaderView
+import io.getstream.chat.android.ui.channel.list.header.viewmodel.ChannelListHeaderViewModel
+import io.getstream.chat.android.ui.channel.list.header.viewmodel.bindView
+import io.getstream.chat.android.ui.channel.list.viewmodel.ChannelListViewModel
+import io.getstream.chat.android.ui.channel.list.viewmodel.factory.ChannelListViewModelFactory
+import io.getstream.slackclonesample.R
+import io.getstream.slackclonesample.databinding.ActivityMainBinding
+import io.getstream.slackclonesample.domain.user.SampleUser
+import io.getstream.slackclonesample.feature.home.channel.bindListView
+import kotlinx.coroutines.flow.collect
+
+class HomeActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityMainBinding
+ private val channelListViewModel: ChannelListViewModel by viewModels { ChannelListViewModelFactory() }
+ private val homeViewModel: HomeViewModel by viewModels {
+ viewModelProviderFactoryOf { HomeViewModelFactory().create() }
+ }
+
+ private val logger = ChatLogger.get("HomeActivity")
+
+ private var isFirstTimeSetupDone: Boolean = false
+
+ private val TAG = "HomeActivity"
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ init()
+ setupViews()
+
+ lifecycleScope.launchWhenStarted {
+ homeViewModel.state.collect { renderState(it) }
+ }
+ }
+
+ override fun onBackPressed() {
+ if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ binding.drawerLayout.closeDrawer(GravityCompat.START, true)
+ }
+ super.onBackPressed()
+ }
+
+ private fun renderState(state: HomeViewModel.State) {
+ if (state.isDrawerOpen && !binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ binding.drawerLayout.openDrawer(GravityCompat.START, true)
+ } else if (!state.isDrawerOpen && binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ binding.drawerLayout.closeDrawer(GravityCompat.START, true)
+ }
+
+ state.cid?.let {
+ supportFragmentManager.commit {
+ val bundle = bundleOf("cid" to it)
+ replace(R.id.chatContainer, args = bundle)
+ }
+ }
+ }
+
+ private fun setupViews() {
+ binding.navigationView.setOnApplyWindowInsetsListener { view, insets ->
+ view.updatePadding(top = insets.systemWindowInsetTop)
+ insets
+ }
+ // Instantiate the ViewModel
+ val channelListHeaderViewModel: ChannelListHeaderViewModel by viewModels()
+
+ val headerView = binding.navigationView.getHeaderView(0) as ChannelListHeaderView
+ // Bind the ViewModel with ChannelListView
+ channelListHeaderViewModel.bindView(
+ headerView,
+ this
+ )
+ headerView.doOnLayout {
+ val layoutParams: FrameLayout.LayoutParams =
+ binding.channelsView.layoutParams as FrameLayout.LayoutParams
+ layoutParams.topMargin = headerView.height
+ }
+
+ binding.channelsView.setChannelClickListener {
+ homeViewModel.submitAction(HomeViewModel.Action.SelectChannelAction(it.cid))
+ }
+
+ channelListViewModel.bindListView(binding.channelsView, this)
+
+ // Handles correct drawer state
+ binding.drawerLayout.addDrawerListener(object : DrawerLayout.SimpleDrawerListener() {
+ override fun onDrawerClosed(drawerView: View) {
+ super.onDrawerClosed(drawerView)
+ homeViewModel.submitAction(HomeViewModel.Action.CloseDrawerAction)
+ }
+
+ override fun onDrawerOpened(drawerView: View) {
+ super.onDrawerOpened(drawerView)
+ homeViewModel.submitAction(HomeViewModel.Action.OpenDrawerAction)
+ }
+ })
+ }
+
+ private fun init() {
+ val user = SlackSampleApp.instance.userRepository.getUser()
+ if (user != SampleUser.None) {
+ authenticateUser(user)
+ } else {
+ authenticateUser(AppConfig.availableUsers[0])
+ }
+ }
+
+ private fun authenticateUser(user: SampleUser) {
+ SlackSampleApp.instance.userRepository.setUser(user)
+ val chatUser = User().apply {
+ id = user.id
+ image = user.image
+ name = user.name
+ }
+
+ ChatClient.instance().connectUser(chatUser, user.token)
+ .enqueue { result ->
+ if (result.isSuccess) {
+ homeViewModel.submitAction(HomeViewModel.Action.UserConnectedAction(result.data().user.id))
+ observeChannels()
+ } else {
+ //_events.postValue(Event(UiEvent.Error(result.error().message)))
+ logger.logD("Failed to set user ${result.error()}")
+ }
+ }
+ // _events.postValue(Event(UiEvent.RedirectToChannels))
+ }
+
+ private fun observeChannels() {
+ channelListViewModel.state.observe(this) {
+ if (!it.isLoading && it.channels.isNotEmpty() && !isFirstTimeSetupDone) {
+ homeViewModel.submitAction(HomeViewModel.Action.SelectChannelAction(it.channels[0].cid))
+ isFirstTimeSetupDone = true
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/HomeViewModel.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/HomeViewModel.kt
new file mode 100644
index 00000000..3a58e952
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/HomeViewModel.kt
@@ -0,0 +1,72 @@
+package io.getstream.slackclonesample.feature.home
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import io.getstream.slackclonesample.util.Factory
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+class HomeViewModel() : ViewModel() {
+
+ private val actions = MutableSharedFlow()
+
+ private val _events = MutableSharedFlow(extraBufferCapacity = 100)
+
+ val events: Flow
+ get() = _events.asSharedFlow()
+
+ private val _state = MutableStateFlow(State(true))
+ private val stateMutex = Mutex()
+
+ val state: StateFlow
+ get() = _state.asStateFlow()
+
+ init {
+ viewModelScope.launch {
+ actions.collect { action ->
+ when (action) {
+ is Action.OpenDrawerAction -> setState { copy(isDrawerOpen = true) }
+ is Action.CloseDrawerAction -> setState { copy(isDrawerOpen = false) }
+ is Action.SelectChannelAction -> setState { copy(
+ isDrawerOpen = false,
+ cid = action.cid
+ ) }
+ is Action.UserConnectedAction -> setState { copy(userId = action.userId) }
+ }
+ }
+ }
+ }
+
+ fun submitAction(action: Action) {
+ viewModelScope.launch {
+ actions.emit(action)
+ }
+ }
+
+ protected suspend fun setState(reducer: State.() -> State) {
+ stateMutex.withLock {
+ _state.value = reducer(_state.value)
+ }
+ }
+
+ data class State(val isDrawerOpen: Boolean, val cid: String? = null, val userId: String? = null)
+
+ sealed class Action {
+ data class UserConnectedAction(val userId: String): Action()
+ object OpenDrawerAction : Action()
+ object CloseDrawerAction: Action()
+ data class SelectChannelAction(val cid: String): Action()
+ }
+
+ sealed class Event {
+ }
+}
+
+class HomeViewModelFactory() : Factory {
+
+ override fun create(): HomeViewModel {
+ return HomeViewModel()
+ }
+}
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListAdapter.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListAdapter.kt
new file mode 100644
index 00000000..3d415958
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListAdapter.kt
@@ -0,0 +1,56 @@
+package io.getstream.slackclonesample.feature.home.channel
+
+import android.view.ViewGroup
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import io.getstream.slackclonesample.feature.home.channel.viewholder.ChannelItemViewHolder
+import io.getstream.slackclonesample.feature.home.channel.viewholder.ChannelSectionViewHolder
+import io.getstream.slackclonesample.feature.home.channel.viewholder.SearchHeaderViewHolder
+import io.getstream.chat.android.ui.channel.list.ChannelListView
+
+class SlackChannelListAdapter(private val channelClickListener: ChannelListView.ChannelClickListener) :
+ ListAdapter(SlackChannelListItemDiffCallback) {
+
+ override fun getItemViewType(position: Int): Int {
+ return when {
+ position == 0 -> {
+ SEARCH_HEADER
+ }
+ getItem(position - 1) is SlackChannelListItem.SectionHeader -> {
+ CHANNEL_SECTION_HEADER
+ }
+ else -> CHANNEL_ITEM
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ return when (viewType) {
+ SEARCH_HEADER -> SearchHeaderViewHolder(parent)
+ CHANNEL_SECTION_HEADER -> ChannelSectionViewHolder(parent)
+ CHANNEL_ITEM -> ChannelItemViewHolder(parent, channelClickListener)
+ else -> throw IllegalArgumentException("Unhandled ChannelList view type: $viewType")
+ }
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ when (holder) {
+ is ChannelItemViewHolder -> {
+ holder.bind((getItem(position - 1) as SlackChannelListItem.ChannelItem).channel)
+ }
+ is ChannelSectionViewHolder -> {
+ val sectionHeader = getItem(position - 1) as SlackChannelListItem.SectionHeader
+ holder.bind(sectionHeader)
+ }
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return super.getItemCount() + 1 // Adding 1 here for search header
+ }
+
+ companion object {
+ const val SEARCH_HEADER = 756
+ const val CHANNEL_SECTION_HEADER = 757
+ const val CHANNEL_ITEM = 758
+ }
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListItemDiffCallback.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListItemDiffCallback.kt
new file mode 100644
index 00000000..7d1a6498
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListItemDiffCallback.kt
@@ -0,0 +1,82 @@
+package io.getstream.slackclonesample.feature.home.channel
+
+import android.util.Log
+import androidx.recyclerview.widget.DiffUtil
+import io.getstream.chat.android.client.models.Channel
+import io.getstream.chat.android.client.models.Message
+import io.getstream.chat.android.client.models.User
+import io.getstream.chat.android.client.models.name
+import io.getstream.chat.android.offline.ChatDomain
+import io.getstream.chat.android.ui.channel.list.adapter.ChannelListPayloadDiff
+import io.getstream.chat.android.ui.common.extensions.getCreatedAtOrThrow
+import io.getstream.chat.android.ui.common.extensions.isRegular
+import io.getstream.chat.android.ui.common.extensions.isSystem
+
+inline fun Any.cast() = this as AnyT
+
+inline fun Any.safeCast() = this as? AnyT
+
+fun currentUserId(): String = ChatDomain.instance().user.value?.id ?: ""
+
+fun Channel.getUsersWithoutSelf(excludeUserId: String = currentUserId()): List =
+ members.map { it.user }.filterNot { it.id == excludeUserId }
+
+fun Channel.getLastMessage(): Message? =
+ messages.asSequence()
+ .filter { it.createdAt != null || it.createdLocallyAt != null }
+ .filter { it.deletedAt == null }
+ .filter { !it.silent }
+ .filter { it.user.id == ChatDomain.instance().user.value?.id || !it.shadowed }
+ .filter { it.isRegular() || it.isSystem() }
+ .maxByOrNull { it.getCreatedAtOrThrow() }
+
+fun Channel.diff(other: Channel): ChannelListPayloadDiff =
+ ChannelListPayloadDiff(
+ nameChanged = name != other.name,
+ avatarViewChanged = getUsersWithoutSelf() != other.getUsersWithoutSelf(),
+ readStateChanged = read != other.read,
+ lastMessageChanged = getLastMessage() != other.getLastMessage(),
+ unreadCountChanged = unreadCount != other.unreadCount,
+ )
+
+object SlackChannelListItemDiffCallback : DiffUtil.ItemCallback() {
+ private const val TAG = "SlackChannelListItemDif"
+
+ override fun areItemsTheSame(oldItem: SlackChannelListItem, newItem: SlackChannelListItem): Boolean {
+ Log.d(TAG, "areItemsTheSame() called with: oldItem = $oldItem, newItem = $newItem")
+ if (oldItem::class != newItem::class) {
+ return false
+ }
+
+ return when (oldItem) {
+ is SlackChannelListItem.ChannelItem -> {
+ oldItem.channel.cid == newItem.safeCast()?.channel?.cid
+ }
+
+ else -> true
+ }
+ }
+
+ override fun areContentsTheSame(oldItem: SlackChannelListItem, newItem: SlackChannelListItem): Boolean {
+ // this is only called if areItemsTheSame returns true, so they must be the same class
+ return when (oldItem) {
+ is SlackChannelListItem.ChannelItem -> {
+ oldItem
+ .channel
+ .diff(newItem.cast().channel)
+ .hasDifference()
+ .not()
+ }
+
+ else -> true
+ }
+ }
+
+ override fun getChangePayload(oldItem: SlackChannelListItem, newItem: SlackChannelListItem): Any {
+ // only called if their contents aren't the same, so they must be channel items and not loading items
+ return oldItem
+ .cast()
+ .channel
+ .diff(newItem.cast().channel)
+ }
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListItemType.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListItemType.kt
new file mode 100644
index 00000000..d62fe24f
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListItemType.kt
@@ -0,0 +1,9 @@
+package io.getstream.slackclonesample.feature.home.channel
+
+import io.getstream.chat.android.client.models.Channel
+
+sealed class SlackChannelListItem {
+ public data class ChannelItem(val channel: Channel) : SlackChannelListItem()
+ public object LoadingMoreItem : SlackChannelListItem()
+ public data class SectionHeader(val sectionTitle: String): SlackChannelListItem()
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListView.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListView.kt
new file mode 100644
index 00000000..9ea6afe6
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListView.kt
@@ -0,0 +1,42 @@
+package io.getstream.slackclonesample.feature.home.channel
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import io.getstream.chat.android.ui.channel.list.ChannelListView
+
+class SlackChannelListView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0,
+) : RecyclerView(context, attrs, defStyle) {
+
+ private lateinit var adapter: SlackChannelListAdapter
+ private lateinit var channelClickListener: ChannelListView.ChannelClickListener
+
+ init {
+ setHasFixedSize(true)
+ layoutManager = LinearLayoutManager(context)
+ }
+
+ fun setChannelClickListener(listener: ChannelListView.ChannelClickListener) {
+ this.channelClickListener = listener
+ }
+
+ fun setChannels(channels: List) {
+ requireAdapter().submitList(channels)
+ }
+
+ private fun requireAdapter(): SlackChannelListAdapter {
+ if (::adapter.isInitialized.not()) {
+ adapter = SlackChannelListAdapter(channelClickListener)
+
+ this.setAdapter(adapter)
+
+ adapter.registerAdapterDataObserver(SnapToTopDataObserver(this))
+ }
+ return adapter
+ }
+
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListViewModelBinding.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListViewModelBinding.kt
new file mode 100644
index 00000000..8f42b779
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SlackChannelListViewModelBinding.kt
@@ -0,0 +1,50 @@
+@file:JvmName("SlackChannelListViewModelBinding")
+package io.getstream.slackclonesample.feature.home.channel
+
+import androidx.lifecycle.LifecycleOwner
+import io.getstream.chat.android.client.models.Channel
+import io.getstream.chat.android.ui.channel.list.ChannelListView
+import io.getstream.chat.android.ui.channel.list.viewmodel.ChannelListViewModel
+
+/**
+ * Binds [ChannelListView] with [ChannelListViewModel], updating the view's state based on
+ * data provided by the ViewModel, and propagating view events to the ViewModel as needed.
+ *
+ * This function sets listeners on the view and ViewModel. Call this method
+ * before setting any additional listeners on these objects yourself.
+ */
+@JvmName("bind")
+fun ChannelListViewModel.bindListView(
+ view: SlackChannelListView,
+ lifecycleOwner: LifecycleOwner,
+) {
+ state.observe(lifecycleOwner) { channelState ->
+ if (!channelState.isLoading) {
+ channelState
+ .channels
+ .transform()
+ .let(view::setChannels)
+ } else {
+ // TODO: handle loading state here.
+ }
+ }
+}
+
+private fun List.transform(): List {
+ val groupChannels = arrayListOf()
+ val directChannels = arrayListOf()
+ val resultantList = arrayListOf()
+
+ this.forEach {
+ if(it.getUsersWithoutSelf().size == 1) {
+ directChannels.add(SlackChannelListItem.ChannelItem(it))
+ } else {
+ groupChannels.add(SlackChannelListItem.ChannelItem(it))
+ }
+ }
+ resultantList.add(SlackChannelListItem.SectionHeader("Channels"))
+ resultantList.addAll(groupChannels)
+ resultantList.add(SlackChannelListItem.SectionHeader("Direct Messages"))
+ resultantList.addAll(directChannels)
+ return resultantList
+}
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SnapToTopDataObserver.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SnapToTopDataObserver.kt
new file mode 100644
index 00000000..232aa6bf
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/SnapToTopDataObserver.kt
@@ -0,0 +1,40 @@
+package io.getstream.slackclonesample.feature.home.channel
+
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * Automatically scrolls [RecyclerView] to the top when the first item is completely visible and
+ * a new range of items is inserted or moved to the position above.
+ */
+internal class SnapToTopDataObserver(
+ private val recyclerView: RecyclerView,
+) : RecyclerView.AdapterDataObserver() {
+
+ val layoutManager = recyclerView.layoutManager as? LinearLayoutManager
+ ?: throw IllegalStateException("Auto scroll only works with LinearLayoutManager")
+
+ override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
+ autoScrollToTopIfNecessary(minOf(fromPosition, toPosition))
+ }
+
+ override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
+ autoScrollToTopIfNecessary(positionStart)
+ }
+
+ /**
+ * Scrolls the list to the top if the user is is not dragging it
+ * and the list is scrolled to the top prior to the update.
+ */
+ private fun autoScrollToTopIfNecessary(itemPosition: Int) {
+ if (
+ itemPosition != 0 ||
+ recyclerView.scrollState != RecyclerView.SCROLL_STATE_IDLE ||
+ recyclerView.canScrollVertically(-1)
+ ) {
+ return
+ }
+
+ layoutManager.scrollToPosition(0)
+ }
+}
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/viewholder/ChannelItemViewHolder.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/viewholder/ChannelItemViewHolder.kt
new file mode 100644
index 00000000..ac9f053b
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/viewholder/ChannelItemViewHolder.kt
@@ -0,0 +1,43 @@
+package io.getstream.slackclonesample.feature.home.channel.viewholder
+
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import io.getstream.chat.android.client.models.Channel
+import io.getstream.chat.android.ui.channel.list.ChannelListView
+import io.getstream.chat.android.ui.common.extensions.getDisplayName
+import io.getstream.slackclonesample.databinding.CustomChannelListItemBinding
+
+class ChannelItemViewHolder(
+ parent: ViewGroup,
+ private val channelClickListener: ChannelListView.ChannelClickListener,
+ private val binding: CustomChannelListItemBinding = CustomChannelListItemBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+) : RecyclerView.ViewHolder(binding.root) {
+
+ private lateinit var channel: Channel
+
+ init {
+ binding.root.setOnClickListener { channelClickListener.onClick(channel) }
+ }
+
+ fun bind(channel: Channel) {
+ this.channel = channel
+
+ binding.apply {
+ nameTextView.text = "# ${channel.getDisplayName(itemView.context)}"
+ channel.unreadCount?.let {
+ Log.d("ChannelItemViewHolder", "${channel.getDisplayName(itemView.context)} $it")
+ if(it > 0) {
+ unreadCountBadge.text = "$it"
+ unreadCountBadge.visibility = View.VISIBLE
+ } else unreadCountBadge.visibility = View.GONE
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/viewholder/ChannelSectionViewHolder.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/viewholder/ChannelSectionViewHolder.kt
new file mode 100644
index 00000000..f6ab15cf
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/viewholder/ChannelSectionViewHolder.kt
@@ -0,0 +1,24 @@
+package io.getstream.slackclonesample.feature.home.channel.viewholder
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import io.getstream.slackclonesample.databinding.ItemChannelSectionHeaderBinding
+import io.getstream.slackclonesample.feature.home.channel.SlackChannelListItem
+
+class ChannelSectionViewHolder(
+ parent: ViewGroup,
+ private val binding: ItemChannelSectionHeaderBinding = ItemChannelSectionHeaderBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(header: SlackChannelListItem.SectionHeader) {
+
+ binding.apply {
+ sectionTitle.text = header.sectionTitle
+ }
+ }
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/viewholder/SearchHeaderViewHolder.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/viewholder/SearchHeaderViewHolder.kt
new file mode 100644
index 00000000..9cc23432
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/feature/home/channel/viewholder/SearchHeaderViewHolder.kt
@@ -0,0 +1,15 @@
+package io.getstream.slackclonesample.feature.home.channel.viewholder
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import io.getstream.slackclonesample.databinding.ItemChannelSearchHeaderBinding
+
+class SearchHeaderViewHolder(
+ parent: ViewGroup,
+ private val binding: ItemChannelSearchHeaderBinding = ItemChannelSearchHeaderBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+) : RecyclerView.ViewHolder(binding.root)
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/util/Extensions.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/util/Extensions.kt
new file mode 100644
index 00000000..c6d47165
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/util/Extensions.kt
@@ -0,0 +1,30 @@
+package io.getstream.slackclonesample.util
+
+import android.view.WindowManager
+import android.widget.Toast
+import androidx.annotation.StringRes
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+
+fun Fragment.showToast(@StringRes resId: Int) {
+ showToast(getString(resId))
+}
+
+fun Fragment.showToast(text: String) {
+ Toast.makeText(activity, text, Toast.LENGTH_SHORT).show()
+}
+
+fun Fragment.useAdjustResize() = setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
+
+private fun Fragment.setSoftInputMode(flags: Int) {
+ (activity ?: return).window.setSoftInputMode(flags)
+}
+
+inline fun viewModelProviderFactoryOf(
+ crossinline f: () -> VM
+): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T = f() as T
+}
+
diff --git a/slack-clone-sample/src/main/java/io/getstream/slackclonesample/util/Factory.kt b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/util/Factory.kt
new file mode 100644
index 00000000..d5095b1d
--- /dev/null
+++ b/slack-clone-sample/src/main/java/io/getstream/slackclonesample/util/Factory.kt
@@ -0,0 +1,6 @@
+package io.getstream.slackclonesample.util
+
+interface Factory {
+
+ fun create(): T
+}
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/slack-clone-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..2b068d11
--- /dev/null
+++ b/slack-clone-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/drawable/bg_red_circle.xml b/slack-clone-sample/src/main/res/drawable/bg_red_circle.xml
new file mode 100644
index 00000000..f86b4493
--- /dev/null
+++ b/slack-clone-sample/src/main/res/drawable/bg_red_circle.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/drawable/ic_baseline_send_24.xml b/slack-clone-sample/src/main/res/drawable/ic_baseline_send_24.xml
new file mode 100644
index 00000000..43334716
--- /dev/null
+++ b/slack-clone-sample/src/main/res/drawable/ic_baseline_send_24.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/slack-clone-sample/src/main/res/drawable/ic_delete.xml b/slack-clone-sample/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 00000000..c678761d
--- /dev/null
+++ b/slack-clone-sample/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/slack-clone-sample/src/main/res/drawable/ic_launcher_background.xml b/slack-clone-sample/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..07d5da9c
--- /dev/null
+++ b/slack-clone-sample/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/slack-clone-sample/src/main/res/drawable/ic_outline_add_circle_outline_24.xml b/slack-clone-sample/src/main/res/drawable/ic_outline_add_circle_outline_24.xml
new file mode 100644
index 00000000..a1c721df
--- /dev/null
+++ b/slack-clone-sample/src/main/res/drawable/ic_outline_add_circle_outline_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/slack-clone-sample/src/main/res/drawable/ic_round_menu_24.xml b/slack-clone-sample/src/main/res/drawable/ic_round_menu_24.xml
new file mode 100644
index 00000000..ff67e556
--- /dev/null
+++ b/slack-clone-sample/src/main/res/drawable/ic_round_menu_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/slack-clone-sample/src/main/res/drawable/ic_round_search_24.xml b/slack-clone-sample/src/main/res/drawable/ic_round_search_24.xml
new file mode 100644
index 00000000..c1818d50
--- /dev/null
+++ b/slack-clone-sample/src/main/res/drawable/ic_round_search_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/slack-clone-sample/src/main/res/font/lato.xml b/slack-clone-sample/src/main/res/font/lato.xml
new file mode 100644
index 00000000..015fa0c8
--- /dev/null
+++ b/slack-clone-sample/src/main/res/font/lato.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/slack-clone-sample/src/main/res/font/lato_bold.xml b/slack-clone-sample/src/main/res/font/lato_bold.xml
new file mode 100644
index 00000000..e12d82bb
--- /dev/null
+++ b/slack-clone-sample/src/main/res/font/lato_bold.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/slack-clone-sample/src/main/res/layout/activity_main.xml b/slack-clone-sample/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..f7bd84a9
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/activity_main.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/layout/confirmation_dialog_fragment.xml b/slack-clone-sample/src/main/res/layout/confirmation_dialog_fragment.xml
new file mode 100644
index 00000000..211c3b72
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/confirmation_dialog_fragment.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/slack-clone-sample/src/main/res/layout/custom_channel_list_item.xml b/slack-clone-sample/src/main/res/layout/custom_channel_list_item.xml
new file mode 100644
index 00000000..689496f4
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/custom_channel_list_item.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/layout/custom_message_input.xml b/slack-clone-sample/src/main/res/layout/custom_message_input.xml
new file mode 100644
index 00000000..5bf3722e
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/custom_message_input.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/slack-clone-sample/src/main/res/layout/custom_message_list_header.xml b/slack-clone-sample/src/main/res/layout/custom_message_list_header.xml
new file mode 100644
index 00000000..83987fd9
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/custom_message_list_header.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/layout/fragment_chat.xml b/slack-clone-sample/src/main/res/layout/fragment_chat.xml
new file mode 100644
index 00000000..477adbe2
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/fragment_chat.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/slack-clone-sample/src/main/res/layout/fragment_home.xml b/slack-clone-sample/src/main/res/layout/fragment_home.xml
new file mode 100644
index 00000000..77d9ef65
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/fragment_home.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/layout/header_channel_list.xml b/slack-clone-sample/src/main/res/layout/header_channel_list.xml
new file mode 100644
index 00000000..caa1254a
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/header_channel_list.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/layout/item_channel_search_header.xml b/slack-clone-sample/src/main/res/layout/item_channel_search_header.xml
new file mode 100644
index 00000000..5e6ee52a
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/item_channel_search_header.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/layout/item_channel_section_header.xml b/slack-clone-sample/src/main/res/layout/item_channel_section_header.xml
new file mode 100644
index 00000000..20b1def0
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/item_channel_section_header.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/layout/item_date_divider.xml b/slack-clone-sample/src/main/res/layout/item_date_divider.xml
new file mode 100644
index 00000000..7b7feab4
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/item_date_divider.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/slack-clone-sample/src/main/res/layout/item_message_giphy.xml b/slack-clone-sample/src/main/res/layout/item_message_giphy.xml
new file mode 100644
index 00000000..a32c2d04
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/item_message_giphy.xml
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/slack-clone-sample/src/main/res/layout/item_message_list.xml b/slack-clone-sample/src/main/res/layout/item_message_list.xml
new file mode 100644
index 00000000..d01bff8c
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/item_message_list.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/slack-clone-sample/src/main/res/layout/item_text_attachment.xml b/slack-clone-sample/src/main/res/layout/item_text_attachment.xml
new file mode 100644
index 00000000..18063f2a
--- /dev/null
+++ b/slack-clone-sample/src/main/res/layout/item_text_attachment.xml
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/slack-clone-sample/src/main/res/menu/menu_top_bar.xml b/slack-clone-sample/src/main/res/menu/menu_top_bar.xml
new file mode 100644
index 00000000..82c4f955
--- /dev/null
+++ b/slack-clone-sample/src/main/res/menu/menu_top_bar.xml
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/slack-clone-sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/slack-clone-sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/slack-clone-sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/slack-clone-sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/mipmap-hdpi/ic_launcher.png b/slack-clone-sample/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..a571e600
Binary files /dev/null and b/slack-clone-sample/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/slack-clone-sample/src/main/res/mipmap-hdpi/ic_launcher_round.png b/slack-clone-sample/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..61da551c
Binary files /dev/null and b/slack-clone-sample/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/slack-clone-sample/src/main/res/mipmap-mdpi/ic_launcher.png b/slack-clone-sample/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..c41dd285
Binary files /dev/null and b/slack-clone-sample/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/slack-clone-sample/src/main/res/mipmap-mdpi/ic_launcher_round.png b/slack-clone-sample/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..db5080a7
Binary files /dev/null and b/slack-clone-sample/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/slack-clone-sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/slack-clone-sample/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..6dba46da
Binary files /dev/null and b/slack-clone-sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/slack-clone-sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/slack-clone-sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..da31a871
Binary files /dev/null and b/slack-clone-sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/slack-clone-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/slack-clone-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..15ac6817
Binary files /dev/null and b/slack-clone-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/slack-clone-sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/slack-clone-sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..b216f2d3
Binary files /dev/null and b/slack-clone-sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/slack-clone-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/slack-clone-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..f25a4197
Binary files /dev/null and b/slack-clone-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/slack-clone-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/slack-clone-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..e96783cc
Binary files /dev/null and b/slack-clone-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/slack-clone-sample/src/main/res/values-night/themes.xml b/slack-clone-sample/src/main/res/values-night/themes.xml
new file mode 100644
index 00000000..e253f9b7
--- /dev/null
+++ b/slack-clone-sample/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/values/colors.xml b/slack-clone-sample/src/main/res/values/colors.xml
new file mode 100644
index 00000000..d7ce93e4
--- /dev/null
+++ b/slack-clone-sample/src/main/res/values/colors.xml
@@ -0,0 +1,13 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
+ #3F0E40
+ #FF0000
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/values/font_certs.xml b/slack-clone-sample/src/main/res/values/font_certs.xml
new file mode 100644
index 00000000..d2226ac0
--- /dev/null
+++ b/slack-clone-sample/src/main/res/values/font_certs.xml
@@ -0,0 +1,17 @@
+
+
+
+ - @array/com_google_android_gms_fonts_certs_dev
+ - @array/com_google_android_gms_fonts_certs_prod
+
+
+ -
+ MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
+
+
+
+ -
+ MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
+
+
+
diff --git a/slack-clone-sample/src/main/res/values/preloaded_fonts.xml b/slack-clone-sample/src/main/res/values/preloaded_fonts.xml
new file mode 100644
index 00000000..5eec405f
--- /dev/null
+++ b/slack-clone-sample/src/main/res/values/preloaded_fonts.xml
@@ -0,0 +1,7 @@
+
+
+
+ - @font/lato
+ - @font/lato_bold
+
+
diff --git a/slack-clone-sample/src/main/res/values/strings.xml b/slack-clone-sample/src/main/res/values/strings.xml
new file mode 100644
index 00000000..8a352d16
--- /dev/null
+++ b/slack-clone-sample/src/main/res/values/strings.xml
@@ -0,0 +1,11 @@
+
+ Slack Sample App
+ Something went wrong!
+ Cancel
+ File too big - max %d mb
+ Search
+ Search
+ More
+ More
+ Jump to
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/values/styles.xml b/slack-clone-sample/src/main/res/values/styles.xml
new file mode 100644
index 00000000..95d1d930
--- /dev/null
+++ b/slack-clone-sample/src/main/res/values/styles.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/slack-clone-sample/src/main/res/values/themes.xml b/slack-clone-sample/src/main/res/values/themes.xml
new file mode 100644
index 00000000..262e0fd9
--- /dev/null
+++ b/slack-clone-sample/src/main/res/values/themes.xml
@@ -0,0 +1,18 @@
+
+
+
+
\ No newline at end of file