├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── kotlin
│ └── com
│ │ └── pusher
│ │ └── chatkitdemo
│ │ ├── ChatKitDemoApp.kt
│ │ ├── EntryActivity.kt
│ │ ├── LangUtil.kt
│ │ ├── MainActivity.kt
│ │ ├── arch
│ │ └── viewModels.kt
│ │ ├── navigation
│ │ └── navigation.kt
│ │ ├── parallel
│ │ ├── Channels.kt
│ │ └── LifecycleReceiverChannel.kt
│ │ ├── preferences.kt
│ │ ├── recyclerview
│ │ └── DataAdapter.kt
│ │ ├── room
│ │ ├── RoomActivity.kt
│ │ ├── RoomFragment.kt
│ │ └── rooms.kt
│ │ └── views.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ └── ic_launcher_background.xml
│ ├── layout
│ ├── activity_entry.xml
│ ├── activity_entry_loaded.xml
│ ├── activity_main.xml
│ ├── activity_room.xml
│ ├── fragment_room.xml
│ ├── fragment_room_loaded.xml
│ ├── include_error.xml
│ ├── include_loading.xml
│ ├── item_message.xml
│ ├── item_message_pending.xml
│ └── item_room.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ └── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── ids.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle.properties
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # IDEA
2 | *.iml
3 | .idea/*
4 | !.idea/codeStyleSettings.xml
5 | !.idea/copyright
6 |
7 | # Gradle
8 | .gradle
9 | build
10 | /reports
11 |
12 | # Gradle Android
13 | /local.properties
14 | /captures
15 | gradle
16 | gradlew
17 | gradlew.bat
18 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: android
2 |
3 | # https://stackoverflow.com/a/47726910/458365
4 | before_install:
5 | - yes | sdkmanager "platforms;android-27"
6 |
7 | before_cache:
8 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
9 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
10 |
11 | cache:
12 | directories:
13 | - ${HOME}/.gradle/caches/
14 | - ${HOME}/.gradle/wrapper/
15 | - ${HOME}/.android/build-cache
16 | - ${TRAVIS_BUILD_DIR}/gradle/caches/
17 | - ${TRAVIS_BUILD_DIR}/gradle/wrapper/dists/
18 | - ${TRAVIS_BUILD_DIR}/android-sdk/extras/
19 |
20 | script: ./gradlew build --stacktrace
21 |
22 | android:
23 | components:
24 | - tools
25 | - build-tools-27.0.3
26 | - android-27
27 | - add-on
28 | - extra
29 | licenses:
30 | - 'android-sdk-preview-.+'
31 | - 'android-sdk-license-.+'
32 | - 'google-gdk-license-.+'
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Pusher
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android Slack Clone (using [Chatkit](http://github.com/pusher/chatkit-android))
2 |
3 | [](http://twitter.com/Pusher)
4 | [](https://raw.githubusercontent.com/pusher/android-slack-clone/master/LICENSE)
5 | [](https://codecov.io/gh/pusher/android-slack-clone)
6 | [](https://travis-ci.org/pusher/android-slack-clone)
7 |
8 |
9 | # WIP (watch this space)
10 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 | compileSdkVersion 28
7 | buildToolsVersion '28.0.3'
8 |
9 | defaultConfig {
10 | applicationId "com.pusher.chatkitdemo"
11 | minSdkVersion 19
12 | targetSdkVersion 28
13 | versionCode 1
14 | versionName "1.0"
15 | multiDexEnabled true
16 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
17 |
18 | }
19 |
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
24 | }
25 | debug {
26 | testCoverageEnabled true
27 | }
28 | }
29 |
30 | sourceSets {
31 | main.java.srcDirs += 'src/main/kotlin'
32 | test.java.srcDirs += 'src/test/kotlin'
33 | androidTest.java.srcDirs += 'src/androidTest/kotlin'
34 | }
35 |
36 | }
37 |
38 | androidExtensions {
39 | experimental = true
40 | }
41 |
42 | dependencies {
43 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
44 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
45 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
46 | implementation "com.android.support:appcompat-v7:$android_support_version"
47 | implementation "com.android.support:design:$android_support_version"
48 | implementation "com.android.support.constraint:constraint-layout:$constraints_layout_version"
49 | implementation "android.arch.lifecycle:extensions:$android_arch_version"
50 | implementation "android.arch.lifecycle:common:$android_arch_version"
51 | implementation "com.android.support:customtabs:$android_support_version"
52 | implementation "com.pusher:chatkit-android:$chatkit_android_version"
53 | implementation 'com.google.firebase:firebase-core:16.0.9'
54 | implementation 'com.google.firebase:firebase-messaging:18.0.0'
55 | implementation 'com.android.support:multidex:1.0.3'
56 | implementation 'com.android.support:coordinatorlayout:28.0.0'
57 |
58 | testImplementation "junit:junit:$junit_legacy_version"
59 |
60 | androidTestImplementation "com.android.support.test:runner:$espresso_runner_version"
61 | androidTestImplementation "com.android.support.test.espresso:espresso-core:$espresso_version"
62 | }
63 |
64 | apply plugin: 'com.google.gms.google-services'
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
32 |
33 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
50 |
51 |
52 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/ChatKitDemoApp.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.support.v4.app.Fragment
6 | import com.pusher.chatkit.AndroidChatkitDependencies
7 | import com.pusher.chatkit.ChatManager
8 | import com.pusher.chatkit.ChatkitTokenProvider
9 | import com.pusher.chatkit.CurrentUser
10 | import com.pusher.platform.logger.AndroidLogger
11 | import com.pusher.platform.logger.LogLevel
12 | import com.pusher.platform.logger.Logger
13 | import com.pusher.util.Result
14 | import elements.Error
15 | import kotlinx.coroutines.ExperimentalCoroutinesApi
16 | import kotlinx.coroutines.channels.BroadcastChannel
17 | import kotlinx.coroutines.channels.Channel
18 | import kotlin.properties.Delegates
19 |
20 | private const val INSTANCE_LOCATOR = "v1:us1:05f46048-3763-4482-9cfe-51ff327c3f29"
21 | private const val TOKEN_PROVIDER_ENDPOINT = "https://chatkit-demoauth-server.herokuapp.com/token"
22 |
23 | val Context.app: ChatKitDemoApp
24 | get() = when (applicationContext) {
25 | null -> error("Application context is null")
26 | is ChatKitDemoApp -> applicationContext as ChatKitDemoApp
27 | else -> error("Application context ($applicationContext) is not ${nameOf()}")
28 | }
29 |
30 | val Fragment.app: ChatKitDemoApp
31 | get() = context!!.app
32 |
33 | class ChatKitDemoApp : Application() {
34 |
35 | companion object {
36 | private var maybeApp: ChatKitDemoApp? = null
37 | val app get() = checkNotNull(maybeApp)
38 | }
39 |
40 | val logger: Logger by lazy { AndroidLogger(LogLevel.VERBOSE) }
41 | private val userPreferences by lazy { UserPreferences(this) }
42 |
43 | var userId: String?
44 | get() = userPreferences.userId
45 | set(value) {
46 | userPreferences.userId = value
47 | tryConnect(value, token)
48 | }
49 |
50 | var token: String?
51 | get() = userPreferences.token
52 | set(value) {
53 | userPreferences.token = value
54 | tryConnect(userId, value)
55 | }
56 |
57 | @ExperimentalCoroutinesApi
58 | private fun tryConnect(userId: String?, token: String?) = when {
59 | userId != null && token != null -> connect(userId, token)
60 | else -> Unit // ignore
61 | }
62 |
63 | private lateinit var chat: ChatManager
64 |
65 | override fun onCreate() = super.onCreate().also {
66 | maybeApp = this
67 | }
68 |
69 | @ExperimentalCoroutinesApi
70 | private fun connect(userId: String, token: String) {
71 |
72 | val dependencies = AndroidChatkitDependencies(
73 | tokenProvider = ChatkitTokenProvider(
74 | endpoint = "$TOKEN_PROVIDER_ENDPOINT?token=$token",
75 | userId = userId
76 | ),
77 | context = applicationContext
78 | )
79 |
80 | chat = ChatManager(
81 | instanceLocator = INSTANCE_LOCATOR,
82 | userId = userId,
83 | dependencies = dependencies
84 | )
85 |
86 | chat.connect { result ->
87 | when (result) {
88 | is Result.Success -> {
89 | result.value.let { user ->
90 | currentUser = user
91 | user.enablePushNotifications { result ->
92 | when (result) {
93 | is Result.Success -> {
94 | // Push Notifications Service Enabled!
95 | }
96 |
97 | is Error -> logger.error("Error starting Push Notifications")
98 | }
99 | }
100 | }
101 | }
102 |
103 | is Error -> logger.error("Failed to connect and get the current user")
104 | }
105 | }
106 | }
107 |
108 | @ExperimentalCoroutinesApi
109 | private var currentUser by Delegates.observable(null) { _, _, new ->
110 | new?.let { userBroadcast.offer(it) }
111 | }
112 |
113 | @ExperimentalCoroutinesApi
114 | private val userBroadcast = BroadcastChannel(capacity = Channel.CONFLATED)
115 |
116 | @ExperimentalCoroutinesApi
117 | suspend fun currentUser(): CurrentUser = when (currentUser) {
118 | null -> userBroadcast.openSubscription().receive()
119 | else -> currentUser!!
120 | }
121 |
122 | }
123 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/EntryActivity.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo
2 |
3 | import android.arch.lifecycle.ViewModel
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.os.Looper
7 | import android.support.v7.app.AppCompatActivity
8 | import com.google.gson.GsonBuilder
9 | import com.pusher.chatkit.CurrentUser
10 | import com.pusher.chatkitdemo.ChatKitDemoApp.Companion.app
11 | import com.pusher.chatkitdemo.EntryActivity.State.*
12 | import com.pusher.chatkitdemo.arch.viewModel
13 | import com.pusher.chatkitdemo.navigation.NavigationEvent
14 | import com.pusher.chatkitdemo.navigation.navigationEvent
15 | import com.pusher.chatkitdemo.navigation.openInBrowser
16 | import com.pusher.chatkitdemo.navigation.openMain
17 | import elements.Error
18 | import elements.NetworkError
19 | import kotlinx.android.synthetic.main.activity_entry.*
20 | import kotlinx.android.synthetic.main.activity_entry_loaded.*
21 | import kotlinx.android.synthetic.main.include_error.*
22 | import kotlinx.android.synthetic.main.include_loading.*
23 | import kotlinx.coroutines.*
24 | import kotlinx.coroutines.channels.BroadcastChannel
25 | import kotlinx.coroutines.channels.Channel
26 | import kotlinx.coroutines.channels.consumeEach
27 | import okhttp3.*
28 | import kotlin.properties.Delegates
29 |
30 | class EntryActivity : AppCompatActivity() {
31 |
32 | private val views by lazy { arrayOf(idleLayout, loadedLayout, errorLayout) }
33 | @ExperimentalCoroutinesApi
34 | private val viewModel by lazy { viewModel() }
35 |
36 | override fun onCreate(savedInstanceState: Bundle?) {
37 | super.onCreate(savedInstanceState)
38 | setContentView(R.layout.activity_entry)
39 | }
40 |
41 | @ExperimentalCoroutinesApi
42 | @ObsoleteCoroutinesApi
43 | override fun onStart() {
44 | super.onStart()
45 | // TODO: Investigate potential leak into viewModel
46 | GlobalScope.launch { viewModel.states.consumeEach { state = it } }
47 | }
48 |
49 | @ExperimentalCoroutinesApi
50 | override fun onNewIntent(intent: Intent?) {
51 | super.onNewIntent(intent)
52 | val navigationEvent = intent?.navigationEvent
53 | when (navigationEvent) {
54 | is NavigationEvent.Entry.WithGitHubCode -> viewModel.authorize(navigationEvent.code)// Log.d("TOKEN", "token: ${navigationEvent.code}")
55 | }
56 | }
57 |
58 | @ExperimentalCoroutinesApi
59 | private var state by Delegates.observable(Idle) { _, _, state ->
60 | state.render()
61 | }
62 |
63 | sealed class State {
64 | object Idle : State()
65 | data class RequiresAuth(val authUrl: String) : State()
66 | data class UserIdReady(val userId: String) : State()
67 | data class UserReady(val currentUser: CurrentUser) : State()
68 | data class Failure(val error: Error) : State()
69 | }
70 |
71 | @ExperimentalCoroutinesApi
72 | private fun State.render() = when (this) {
73 | is Idle -> GlobalScope.launch(Dispatchers.Main) { renderIdle() }
74 | is UserIdReady -> GlobalScope.launch(Dispatchers.Main) {
75 | renderIdle()
76 | viewModel.loadUser(userId)
77 | }
78 | is UserReady -> GlobalScope.launch(Dispatchers.Main) { renderUser(currentUser) }
79 | is Failure -> GlobalScope.launch(Dispatchers.Main) { renderFailure(error) }
80 | is RequiresAuth -> GlobalScope.launch(Dispatchers.Main) { openInBrowser(authUrl) }
81 | }
82 |
83 | private fun renderIdle() {
84 | views.showOnly(idleLayout)
85 | loadingTextView.setText(R.string.logging_you_in)
86 | }
87 |
88 | private fun renderUser(user: CurrentUser) {
89 | views.showOnly(loadedLayout)
90 | userNameView.text = "Welcome ${user.name}"
91 | continueButton.setOnClickListener {
92 | openMain(user.id)
93 | }
94 | }
95 |
96 | @ExperimentalCoroutinesApi
97 | private fun renderFailure(error: Error) {
98 | views.showOnly(errorLayout)
99 | errorMessageView.text = "$error"
100 | retryButton.setOnClickListener {
101 | state = Idle
102 | viewModel.load()
103 | }
104 | }
105 |
106 | }
107 |
108 | @ExperimentalCoroutinesApi
109 | class EntryViewModel : ViewModel() {
110 |
111 | private object Github {
112 | private const val gitHubClientId = "20cdd317000f92af12fe"
113 | private const val callbackUri = "https://chatkit-demoauth-server.herokuapp.com/success?url=chatkit://auth"
114 |
115 | const val gitHubAuthUrl = "https://github.com/login/oauth/authorize" +
116 | "?client_id=$gitHubClientId" +
117 | "&scope=user:email" +
118 | "&redirect_uri=$callbackUri"
119 | }
120 |
121 | @ExperimentalCoroutinesApi
122 | private val stateBroadcast = BroadcastChannel(Channel.CONFLATED)
123 | @ExperimentalCoroutinesApi
124 | val states get() = stateBroadcast.openSubscription()
125 |
126 | private val client by lazy { OkHttpClient() }
127 | private val gson by lazy { GsonBuilder().create() }
128 |
129 | init {
130 | stateBroadcast.offer(Idle)
131 | load()
132 | }
133 |
134 | fun load() = GlobalScope.launch {
135 | app.userId.let { id ->
136 | when (id) {
137 | null -> RequiresAuth(Github.gitHubAuthUrl)
138 | else -> UserIdReady(id)
139 | }.let { stateBroadcast.send(it) }
140 | }
141 | }
142 |
143 | suspend fun loadUser(userId: String) {
144 | app.userId = userId
145 | val state = UserReady(app.currentUser())
146 | stateBroadcast.send(state)
147 | }
148 |
149 | override fun onCleared() {
150 | stateBroadcast.close()
151 | }
152 |
153 | data class AuthResponseBody(val id: String, val token: String)
154 | data class AuthRequestBody(val code: String)
155 |
156 | fun authorize(code: String) {
157 | GlobalScope.launch {
158 | if (Looper.myLooper() == null) Looper.prepare()
159 | val requestBody = RequestBody.create(MediaType.parse("text/plain"), AuthRequestBody(code).toJson())
160 | val request = Request.Builder().apply {
161 | url("https://chatkit-demoauth-server.herokuapp.com/auth")
162 | post(requestBody)
163 | }.build()
164 | val response = client.newCall(request).execute()
165 | val responseBody = response.body()?.fromJson()
166 | when (responseBody) {
167 | null -> stateBroadcast.offer(Failure(NetworkError("Oops! response: $response")))
168 | else -> responseBody.let { (id, token) ->
169 | app.userId = id
170 | app.token = token
171 | stateBroadcast.send(UserIdReady(id))
172 | }
173 | }
174 | }
175 | }
176 |
177 | private fun A.toJson(): String =
178 | gson.toJson(this)
179 |
180 | private inline fun ResponseBody.fromJson(): A =
181 | gson.fromJson(this.charStream(), A::class.java)
182 |
183 | }
184 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/LangUtil.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo
2 |
3 | /**
4 | * Simplified way to get the name of a type
5 | */
6 | inline fun nameOf(): String = nameOf(T::class.java)
7 |
8 | fun nameOf(type: Class): String = type.simpleName
9 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Bundle
5 | import android.support.v7.app.AppCompatActivity
6 | import android.support.v7.widget.LinearLayoutManager
7 | import android.view.View.GONE
8 | import android.view.View.VISIBLE
9 | import com.pusher.chatkit.rooms.Room
10 | import com.pusher.chatkitdemo.MainActivity.State.Loaded
11 | import com.pusher.chatkitdemo.navigation.open
12 | import com.pusher.chatkitdemo.recyclerview.dataAdapterFor
13 | import com.pusher.chatkitdemo.room.coolName
14 | import elements.Error
15 | import kotlinx.android.synthetic.main.activity_main.*
16 | import kotlinx.android.synthetic.main.item_room.*
17 | import kotlinx.coroutines.GlobalScope
18 | import kotlinx.coroutines.launch
19 | import kotlin.properties.Delegates
20 | import kotlinx.coroutines.Dispatchers
21 | import kotlinx.coroutines.ExperimentalCoroutinesApi
22 | import kotlinx.android.synthetic.main.activity_main.room_list as roomListView
23 |
24 | import android.util.Log
25 |
26 | class MainActivity : AppCompatActivity() {
27 |
28 | private val adapter = dataAdapterFor(R.layout.item_room) { room: Room ->
29 | @SuppressLint("SetTextI18n")
30 | roomNameView.text = room.coolName
31 | roomItemLayout.setOnClickListener {
32 | open(room)
33 | }
34 | }
35 |
36 | private var state by Delegates.observable(State.Idle) { _, _, state ->
37 | state.render()
38 | }
39 |
40 | @ExperimentalCoroutinesApi
41 | override fun onCreate(savedInstanceState: Bundle?) {
42 | super.onCreate(savedInstanceState)
43 | setContentView(R.layout.activity_main)
44 | setSupportActionBar(toolbar)
45 |
46 | roomListView.adapter = adapter
47 | roomListView.layoutManager = LinearLayoutManager(this)
48 |
49 | state = State.Idle
50 |
51 |
52 |
53 | GlobalScope.launch {
54 | state = Loaded(app.currentUser().rooms.toSet())
55 |
56 | }
57 |
58 |
59 | }
60 |
61 | private fun State.render() = when (this) {
62 | is State.Idle -> GlobalScope.launch(Dispatchers.Main) { renderIdle() }
63 | is State.Loaded -> GlobalScope.launch(Dispatchers.Main) { renderLoaded(rooms) }
64 | is State.Failed -> GlobalScope.launch(Dispatchers.Main) { renderFailed(error) }
65 | }
66 |
67 | private fun renderIdle() {
68 | progress.visibility = VISIBLE
69 | roomListView.visibility = GONE
70 | errorView.visibility = GONE
71 | }
72 |
73 | private fun renderLoaded(rooms: Set) {
74 | progress.visibility = GONE
75 | roomListView.visibility = VISIBLE
76 | errorView.visibility = GONE
77 | adapter.data = rooms.filter { it.memberUserIds.size < 100 }
78 | }
79 |
80 | private fun renderFailed(error: Error) {
81 | progress.visibility = GONE
82 | roomListView.visibility = GONE
83 | errorView.visibility = VISIBLE
84 | errorView.text = "$error"
85 | }
86 |
87 | sealed class State {
88 | object Idle : State()
89 | data class Loaded(val rooms: Set) : State()
90 | data class Failed(val error: Error) : State()
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/arch/viewModels.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo.arch
2 |
3 | import android.arch.lifecycle.ViewModel
4 | import android.arch.lifecycle.ViewModelProviders
5 | import android.support.v4.app.Fragment
6 | import android.support.v4.app.FragmentActivity
7 |
8 | inline fun FragmentActivity.viewModel() =
9 | ViewModelProviders.of(this).get(A::class.java)
10 |
11 | inline fun Fragment.viewModel() =
12 | ViewModelProviders.of(this).get(A::class.java)
13 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/navigation/navigation.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo.navigation
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.UriMatcher
7 | import android.net.Uri
8 | import android.support.customtabs.CustomTabsIntent
9 | import com.pusher.chatkit.rooms.Room
10 | import com.pusher.chatkitdemo.app
11 |
12 | private enum class Screen(val path: String, val authority: String = "chat.pusher.com") {
13 | ROOM("room/#") {
14 | override fun asNavigationEvent(uri: Uri): NavigationEvent =
15 | NavigationEvent.room(uri.lastPathSegment)
16 | },
17 | MAIN("home") {
18 | override fun asNavigationEvent(uri: Uri): NavigationEvent =
19 | NavigationEvent.main(uri.getQueryParameter("userId"))
20 | },
21 | ENTRY("/", "auth") {
22 | override fun asNavigationEvent(uri: Uri): NavigationEvent = with(uri) {
23 | val token = getQueryParameter("code")
24 | when (token) {
25 | null -> NavigationEvent.missingGitHubToken()
26 | else -> NavigationEvent.withGitHubToken(token)
27 | }
28 | }
29 |
30 | };
31 |
32 | abstract fun asNavigationEvent(uri: Uri): NavigationEvent
33 |
34 | companion object {
35 | fun find(ordinal: Int): Screen? =
36 | values().getOrNull(ordinal)
37 | }
38 | }
39 |
40 | sealed class NavigationEvent {
41 |
42 | companion object {
43 | @JvmStatic
44 | fun room(roomId: String): NavigationEvent = Room(roomId)
45 |
46 | @JvmStatic
47 | fun main(userId: String): NavigationEvent = Main(userId)
48 |
49 | @JvmStatic
50 | fun withGitHubToken(token: String): NavigationEvent = Entry.WithGitHubCode(token)
51 |
52 | @JvmStatic
53 | fun missingGitHubToken(): NavigationEvent = Entry.MissingGitHubToken
54 |
55 | @JvmStatic
56 | fun missingUri(): NavigationEvent = MissingUri
57 |
58 | @JvmStatic
59 | fun unknown(uri: Uri): NavigationEvent = Unknown(uri)
60 | }
61 |
62 | data class Room(val roomId: String) : NavigationEvent()
63 | data class Main(val userId: String) : NavigationEvent()
64 | data class Unknown(val data: Uri) : NavigationEvent()
65 | object MissingUri : NavigationEvent()
66 | sealed class Entry : NavigationEvent() {
67 | data class WithGitHubCode(val code: String) : Entry()
68 | object MissingGitHubToken : Entry()
69 | }
70 | }
71 |
72 | private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
73 | Screen.values().forEach { screen -> with(screen) { addURI(authority, path, ordinal) } }
74 | }
75 |
76 | val Intent.navigationEvent: NavigationEvent
77 | get() = data.navigationEvent
78 |
79 | val Uri?.navigationEvent: NavigationEvent
80 | get() = when (this) {
81 | null -> NavigationEvent.missingUri()
82 | else -> Screen.find(uriMatcher.match(this))?.asNavigationEvent(this)
83 | ?: NavigationEvent.unknown(this)
84 | }
85 |
86 | fun Context.openIntent(path: String) =
87 | Intent(Intent.ACTION_VIEW, Uri.parse(path)).apply {
88 | `package` = packageName
89 | }
90 |
91 | fun Context.open(path: String) =
92 | startActivity(openIntent(path))
93 |
94 | fun Context.openInBrowser(path: String) =
95 | CustomTabsIntent.Builder().build().launchUrl(this, Uri.parse(path))
96 |
97 | fun Context.open(room: Room) =
98 | open("https://chat.pusher.com/room/${room.id}")
99 |
100 | fun Context.openMain(userId: String) =
101 | open("https://chat.pusher.com/home?userId=$userId")
102 |
103 | fun Activity.failNavigation(navigationEvent: NavigationEvent) {
104 | app.logger.error("Failed to load navigation: $navigationEvent")
105 | finish()
106 | }
107 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/parallel/Channels.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo.parallel
2 |
3 | import kotlinx.coroutines.ExperimentalCoroutinesApi
4 | import kotlinx.coroutines.channels.BroadcastChannel
5 | import kotlinx.coroutines.channels.Channel
6 | import kotlinx.coroutines.launch
7 | import kotlinx.coroutines.GlobalScope
8 |
9 |
10 | @ExperimentalCoroutinesApi
11 | fun lazyBroadcast(block: suspend BroadcastChannel.() -> Unit = {}) =
12 | lazy { broadcast(block) }
13 |
14 | @ExperimentalCoroutinesApi
15 | fun broadcast(block: suspend BroadcastChannel.() -> Unit = {}) =
16 | BroadcastChannel(Channel.CONFLATED).also {
17 | GlobalScope.launch { block(it) }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/parallel/LifecycleReceiverChannel.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo.parallel
2 |
3 | import android.arch.lifecycle.Lifecycle
4 | import android.arch.lifecycle.Lifecycle.Event.*
5 | import android.arch.lifecycle.LifecycleObserver
6 | import android.arch.lifecycle.LifecycleOwner
7 | import android.arch.lifecycle.OnLifecycleEvent
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.channels.BroadcastChannel
10 | import kotlinx.coroutines.channels.Channel
11 | import kotlinx.coroutines.channels.ReceiveChannel
12 | import kotlinx.coroutines.channels.toChannel
13 | import kotlinx.coroutines.launch
14 | import kotlinx.coroutines.GlobalScope
15 | import kotlinx.coroutines.ObsoleteCoroutinesApi
16 |
17 | @ExperimentalCoroutinesApi
18 | fun LifecycleOwner.onLifecycle(block: () -> ReceiveChannel): ReceiveChannel =
19 | LifecycleReceiverChannel(this.lifecycle, block)
20 |
21 | private class LifecycleReceiverChannel @ExperimentalCoroutinesApi constructor(
22 | lifecycle: Lifecycle,
23 | private val block: () -> ReceiveChannel,
24 | private val broadcastChannel: BroadcastChannel = BroadcastChannel(Channel.CONFLATED),
25 | private val subscription: ReceiveChannel = broadcastChannel.openSubscription()
26 | ) : ReceiveChannel by subscription, LifecycleObserver {
27 |
28 | private var channel: ReceiveChannel? = null
29 |
30 | init {
31 | lifecycle.addObserver(this)
32 | }
33 |
34 | @ObsoleteCoroutinesApi
35 | @OnLifecycleEvent(ON_START)
36 | fun onStart() {
37 | channel?.cancel()
38 | channel = block()
39 | GlobalScope.launch { channel?.toChannel(broadcastChannel) }
40 | }
41 |
42 | @OnLifecycleEvent(ON_STOP)
43 | fun onStop() {
44 | channel?.cancel()
45 | }
46 |
47 | @OnLifecycleEvent(ON_DESTROY)
48 | fun onDestroy() {
49 | subscription.cancel()
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/preferences.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo
2 |
3 | import android.content.Context
4 | import android.content.Context.MODE_PRIVATE
5 | import kotlin.properties.ReadWriteProperty
6 | import kotlin.reflect.KProperty
7 |
8 | private const val USER_PREFERENCES_NAME = "USER_PREFERENCES_NAME"
9 |
10 | class UserPreferences(context: Context) {
11 |
12 | private val preferences = SharedPreferencesExtension(context.applicationContext, USER_PREFERENCES_NAME)
13 |
14 | var userId by preferences.string("userId")
15 | var token by preferences.string("token")
16 |
17 | }
18 |
19 | private class SharedPreferencesExtension(context: Context, name: String) {
20 |
21 | private val preferences = context.getSharedPreferences(name, MODE_PRIVATE)
22 |
23 | fun string(key: String) = object : ReadWriteProperty {
24 | override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) =
25 | preferences.edit().putString(key, value).apply()
26 |
27 | override operator fun getValue(thisRef: Any?, property: KProperty<*>): String? =
28 | preferences.getString(key, null)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/recyclerview/DataAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo.recyclerview
2 |
3 | import android.support.annotation.LayoutRes
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import kotlinx.android.extensions.LayoutContainer
9 | import kotlin.properties.Delegates
10 |
11 | fun dataAdapterFor(
12 | layoutRes: Int,
13 | onBind: LayoutContainer.(A) -> Unit
14 | ): DataAdapter = SimpleDataAdapter(layoutRes, onBind)
15 |
16 | fun dataAdapterFor(
17 | block: DataAdapterContext.() -> Unit
18 | ): DataAdapter = MultiDataAdapter(
19 | DataAdapterContextWithMap().apply(block).adapterMap
20 | )
21 |
22 | sealed class DataAdapter : RecyclerView.Adapter>() {
23 |
24 | var data: List by Delegates.observable(emptyList()) { _, _, _ ->
25 | notifyDataSetChanged()
26 | }
27 |
28 | @JvmOverloads
29 | fun insert(item: A, index: Int = 0) {
30 | data = data.subList(0, index) + item + data.subList(index, data.size)
31 | }
32 |
33 | operator fun plusAssign(item: A) =
34 | insert(item)
35 | }
36 |
37 | sealed class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
38 | abstract fun bind(data: A)
39 | }
40 |
41 | typealias ViewHolderFactory = (A) -> DataViewHolder
42 |
43 | /**
44 | * Generalised [RecyclerView.Adapter] for lists of simple items using android extensions for the binding.
45 | */
46 | private class SimpleDataAdapter(
47 | @LayoutRes private val layoutRes: Int,
48 | private val onBind: LayoutContainer.(A) -> Unit
49 | ) : DataAdapter() {
50 |
51 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
52 | BindDataViewHolder(parent, layoutRes, onBind)
53 |
54 | override fun getItemCount(): Int =
55 | data.size
56 |
57 | override fun onBindViewHolder(holder: DataViewHolder, position: Int) =
58 | holder.bind(data[position])
59 |
60 | }
61 |
62 | private class BindDataViewHolder(
63 | parent: ViewGroup,
64 | @LayoutRes layoutRes: Int,
65 | private val onBind: LayoutContainer.(A) -> Unit
66 | ) : DataViewHolder(
67 | LayoutInflater.from(parent.context).inflate(layoutRes, parent, false)
68 | ), LayoutContainer {
69 |
70 | override val containerView: View = itemView
71 |
72 | override fun bind(data: A) {
73 | onBind(data)
74 | }
75 |
76 | }
77 |
78 | private class MultiDataAdapter(
79 | private val adapterMap: Map<(A) -> Boolean, IndexedAdapter>
80 | ) : DataAdapter() {
81 |
82 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataViewHolder =
83 | adapterMap.values.first { it.viewType == viewType }.adapter.invoke(parent)
84 |
85 | override fun getItemCount(): Int =
86 | data.size
87 |
88 | override fun onBindViewHolder(holder: DataViewHolder, position: Int) =
89 | holder.bind(data[position])
90 |
91 | override fun getItemViewType(position: Int): Int =
92 | adapterMap.entries
93 | .first { (accepts, _) -> accepts(data[position]) }.value.viewType
94 |
95 | }
96 |
97 | private data class IndexedAdapter(val viewType: Int, val adapter: (parent: ViewGroup) -> DataViewHolder)
98 |
99 | sealed class DataAdapterContext {
100 | abstract fun on(accept: (A) -> Boolean, @LayoutRes layoutRes: Int, onBind: LayoutContainer.(A) -> Unit)
101 |
102 | inline fun on(@LayoutRes layoutRes: Int, crossinline onBind: LayoutContainer.(B) -> Unit) =
103 | on({ it is B }, layoutRes) { onBind(it as B) }
104 | }
105 |
106 | private class DataAdapterContextWithMap : DataAdapterContext() {
107 |
108 | val adapterMap = mutableMapOf<(A) -> Boolean, IndexedAdapter>()
109 |
110 | override fun on(accept: (A) -> Boolean, @LayoutRes layoutRes: Int, onBind: LayoutContainer.(A) -> Unit) {
111 | adapterMap += accept to IndexedAdapter(adapterMap.size) { parent -> BindDataViewHolder(parent, layoutRes, onBind) }
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/room/RoomActivity.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo.room
2 |
3 | import android.os.Bundle
4 | import android.support.v7.app.AppCompatActivity
5 | import com.pusher.chatkitdemo.R
6 | import com.pusher.chatkitdemo.navigation.NavigationEvent
7 | import com.pusher.chatkitdemo.navigation.failNavigation
8 | import com.pusher.chatkitdemo.navigation.navigationEvent
9 | import kotlinx.android.synthetic.main.activity_room.*
10 | import android.net.Uri.parse as parseUri
11 |
12 | class RoomActivity : AppCompatActivity() {
13 |
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContentView(R.layout.activity_room)
17 |
18 | setSupportActionBar(toolbar)
19 | supportActionBar?.setDisplayShowHomeEnabled(true)
20 | supportActionBar?.setDisplayHomeAsUpEnabled(true)
21 |
22 | val event = intent.navigationEvent
23 | when (event) {
24 | is NavigationEvent.Room -> (roomFragment as RoomFragment).bind(event.roomId)
25 | else -> failNavigation(event)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/room/RoomFragment.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo.room
2 |
3 | import android.arch.lifecycle.Lifecycle.State.STARTED
4 | import android.arch.lifecycle.LifecycleOwner
5 | import android.os.Bundle
6 | import android.os.Looper
7 | import android.support.annotation.UiThread
8 | import android.support.v4.app.Fragment
9 | import android.support.v7.widget.LinearLayoutManager
10 | import android.util.Log
11 | import android.view.LayoutInflater
12 | import android.view.View
13 | import android.view.ViewGroup
14 | import com.pusher.chatkit.messages.multipart.NewPart
15 | import com.pusher.chatkit.messages.multipart.Payload
16 | import com.pusher.chatkit.messages.multipart.Payload.Inline
17 | import com.pusher.chatkit.rooms.Room
18 | import com.pusher.chatkit.rooms.RoomListeners
19 | import com.pusher.chatkitdemo.ChatKitDemoApp.Companion.app
20 | import com.pusher.chatkitdemo.R
21 | import com.pusher.chatkitdemo.recyclerview.dataAdapterFor
22 | import com.pusher.chatkitdemo.room.RoomState.*
23 | import com.pusher.chatkitdemo.showOnly
24 | import com.pusher.util.Result
25 | import elements.Subscription
26 | import kotlinx.android.synthetic.main.fragment_room.*
27 | import kotlinx.android.synthetic.main.fragment_room_loaded.*
28 | import kotlinx.android.synthetic.main.include_error.*
29 | import kotlinx.android.synthetic.main.item_message.*
30 | import kotlinx.coroutines.*
31 | import kotlin.properties.Delegates
32 |
33 | typealias PusherError = elements.Error
34 |
35 | class RoomFragment : Fragment() {
36 |
37 | private val views by lazy { arrayOf(idleLayout, loadedLayout, errorLayout) }
38 |
39 | private var state by Delegates.observable(RoomState.Initial) { _, _, new ->
40 | new.render()
41 | }
42 |
43 | private val adapter = dataAdapterFor- {
44 | on(R.layout.item_message) { (details) ->
45 | userNameView.text = details.userName
46 | messageView.text = details.message
47 | }
48 | on(R.layout.item_message_pending) { (details) ->
49 | userNameView.text = details.userName
50 | messageView.text = details.message
51 | }
52 | on(R.layout.item_message_pending) { (details, _) ->
53 | userNameView.text = details.userName
54 | messageView.text = details.message
55 | }
56 | }
57 |
58 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
59 | inflater.inflate(R.layout.fragment_room, container, false)
60 |
61 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
62 | super.onViewCreated(view, savedInstanceState)
63 |
64 | messageList.adapter = adapter
65 | messageList.layoutManager = LinearLayoutManager(activity).apply {
66 | reverseLayout = false
67 | stackFromEnd = false
68 | isSmoothScrollbarEnabled = true
69 | }
70 | sendButton.setOnClickListener {
71 | messageInput.text.takeIf { it.isNotBlank() }?.let { text ->
72 | state.let { it as? Ready }?.let { it.room.id }?.let { roomId ->
73 | sendMessage(roomId, text.toString())
74 | messageInput.text.clear()
75 | }
76 | }
77 | }
78 | }
79 |
80 | override fun onDestroyView() {
81 | super.onDestroyView()
82 |
83 | state.let { it as? Ready }?.sub?.unsubscribe()
84 | }
85 |
86 | @ExperimentalCoroutinesApi
87 | private fun sendMessage(roomId: String, text: String) = GlobalScope.launch {
88 | app.currentUser().apply {
89 | val item = Item.Pending(Item.Details(name ?: id, text))
90 | addItem(item)
91 |
92 | sendMultipartMessage(
93 | roomId = roomId,
94 | parts = listOf(NewPart.Inline(text, "text/plain")),
95 | callback = { result ->
96 | when (result) {
97 | is Result.Success -> {
98 | removeItem(item)
99 | }
100 | is Result.Failure -> {
101 | removeItem(item)
102 | }
103 | }
104 | }
105 | )
106 | }
107 | }
108 |
109 | @ExperimentalCoroutinesApi
110 | fun bind(roomId: String) = GlobalScope.launch {
111 | // if (Looper.myLooper() == null) Looper.prepare() // Old version of the SDK uses a handle and breaks
112 |
113 | with(app.currentUser()) {
114 | val room = rooms.find { it.id == roomId }
115 | var messagecontent: String = ""
116 | when (room) {
117 | null -> renderFailed(Error("Room not found"))
118 | else -> subscribeToRoomMultipart(
119 | room = room,
120 | messageLimit = 20,
121 | listeners = RoomListeners(
122 | onErrorOccurred = { error ->
123 | state = Failed(error)
124 | },
125 | onMultipartMessage = { message ->
126 | message.parts.forEach { part ->
127 | val payload = part.payload
128 | when (payload) {
129 | is Inline -> {
130 | messagecontent = payload.content
131 | }
132 | is Payload.Url -> {
133 | }
134 | is Payload.Attachment -> {
135 | }
136 | else -> {
137 | }
138 | }
139 | }
140 | Log.d("Slackclone", "Message added" + message.sender?.name)
141 |
142 | addItem(
143 | Item.Loaded(
144 | Item.Details(
145 | userName = message.sender?.name
146 | ?: "???",
147 | message = messagecontent ?: "----"
148 | )
149 | )
150 | )
151 |
152 |
153 | }
154 | ),
155 |
156 |
157 | callback = { subscription ->
158 | Log.d("Slackclone", "Room Ready")
159 | state = Ready(room, subscription, adapter.data)
160 | }
161 | )
162 | }
163 | }
164 | }
165 |
166 | private fun addItem(item: Item) = launchOnUi {
167 | adapter.data = adapter.data + item
168 | messageList.scrollToPosition(adapter.itemCount - 1)
169 | }
170 |
171 |
172 | private fun removeItem(item: Item) = launchOnUi {
173 | adapter.data = adapter.data - item
174 | }
175 |
176 |
177 | private fun RoomState.render(): Job = when (this) {
178 | is Initial -> renderIdle()
179 | is Ready -> renderLoadedCompletely(room, items)
180 | is Failed -> renderFailed(Error("$error"))
181 | }
182 |
183 | @UiThread
184 | private fun renderIdle() = launchOnUi {
185 | views.showOnly(idleLayout)
186 | adapter.data = emptyList()
187 | }
188 |
189 | private fun renderLoadedCompletely(room: Room, messages: List) = launchOnUi {
190 | views.showOnly(loadedLayout)
191 | activity?.title = room.coolName
192 | adapter.data = messages
193 | }
194 |
195 | private fun renderFailed(error: Error) = launchOnUi {
196 | views.showOnly(errorLayout)
197 | errorMessageView.text = error.message
198 | retryButton.visibility = View.GONE // TODO: Retry button
199 | }
200 | }
201 |
202 | private fun LifecycleOwner.launchOnUi(block: suspend CoroutineScope.() -> Unit) = when {
203 | lifecycle.currentState > STARTED -> GlobalScope.launch(context = Dispatchers.Main, block = block)
204 | else -> GlobalScope.launch { Log.d("Slackclone", "Unexpected lifecycle state: ${lifecycle.currentState}") }
205 | }
206 |
207 | sealed class RoomState {
208 |
209 | object Initial : RoomState()
210 | data class Ready(val room: Room, val sub: Subscription, val items: List
- ) : RoomState()
211 | data class Failed(val error: PusherError) : RoomState()
212 |
213 | sealed class Item {
214 | abstract val details: Details
215 |
216 | data class Loaded(override val details: Details) : Item()
217 | data class Pending(override val details: Details) : Item()
218 | data class Failed(override val details: Details, val error: PusherError) : Item()
219 |
220 | data class Details(val userName: String, val message: String)
221 | }
222 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/room/rooms.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo.room
2 |
3 | import com.pusher.chatkit.rooms.Room
4 |
5 | val Room.coolName
6 | get() = "#$name"
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/pusher/chatkitdemo/views.kt:
--------------------------------------------------------------------------------
1 | package com.pusher.chatkitdemo
2 |
3 | import android.view.View
4 | import android.view.View.GONE
5 | import android.view.View.VISIBLE
6 |
7 | fun Array.showOnly(view: View) = forEach {
8 | when (it) {
9 | view -> it.visibility = VISIBLE
10 | else -> it.visibility = GONE
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_entry.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
17 |
18 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_entry_loaded.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
20 |
21 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
20 |
21 |
22 |
23 |
30 |
31 |
36 |
37 |
43 |
44 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_room.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
20 |
21 |
22 |
23 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_room.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
15 |
16 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_room_loaded.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
15 |
16 |
17 |
18 |
19 |
27 |
28 |
33 |
34 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/include_error.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
21 |
22 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/include_loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_message.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_message_pending.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
18 |
19 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_room.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pusher/android-slack-clone/8419084d0a2c9f7e32d0dbf14a269994ccd73354/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pusher/android-slack-clone/8419084d0a2c9f7e32d0dbf14a269994ccd73354/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pusher/android-slack-clone/8419084d0a2c9f7e32d0dbf14a269994ccd73354/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pusher/android-slack-clone/8419084d0a2c9f7e32d0dbf14a269994ccd73354/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pusher/android-slack-clone/8419084d0a2c9f7e32d0dbf14a269994ccd73354/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pusher/android-slack-clone/8419084d0a2c9f7e32d0dbf14a269994ccd73354/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pusher/android-slack-clone/8419084d0a2c9f7e32d0dbf14a269994ccd73354/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pusher/android-slack-clone/8419084d0a2c9f7e32d0dbf14a269994ccd73354/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pusher/android-slack-clone/8419084d0a2c9f7e32d0dbf14a269994ccd73354/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pusher/android-slack-clone/8419084d0a2c9f7e32d0dbf14a269994ccd73354/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ChatKit Demo
3 | continue
4 | Logging you in
5 | Oops! Something went wrong
6 | retry
7 | Loading…
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | kotlin_version = '1.3.31'
6 | coroutines_version = '1.2.1'
7 | android_gradle_version = '3.1.2'
8 | android_support_version = '28.0.0'
9 | android_arch_version = '1.1.1'
10 | mockito_version = '2.10.0'
11 | constraints_layout_version = '1.1.3'
12 | espresso_version = '3.0.1'
13 | espresso_runner_version = '1.0.1'
14 | junit_legacy_version = '4.12'
15 | chatkit_android_version = '1.3.3'
16 | }
17 | repositories {
18 | google()
19 | jcenter()
20 | }
21 | dependencies {
22 | classpath 'com.android.tools.build:gradle:3.4.1'
23 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
24 | classpath 'org.junit.platform:junit-platform-gradle-plugin:1.1.0'
25 | classpath 'de.mannodermaus.gradle.plugins:android-junit5:1.0.31'
26 | classpath 'com.google.gms:google-services:4.2.0'
27 | }
28 | }
29 |
30 | allprojects {
31 | repositories {
32 | mavenLocal()
33 | google()
34 | jcenter()
35 | mavenCentral()
36 | }
37 | }
38 |
39 | task clean(type: Delete) {
40 | delete rootProject.buildDir
41 | }
42 |
--------------------------------------------------------------------------------
/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.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = "android-slack-clone"
2 | include ':app'
3 |
--------------------------------------------------------------------------------