ChatRoomsViewModel.kt
package chat.rocket.android.chatrooms.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import chat.rocket.android.chatrooms.adapter.ItemHolder
import chat.rocket.android.chatrooms.adapter.LoadingItemHolder
import chat.rocket.android.chatrooms.adapter.RoomUiModelMapper
import chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor
import chat.rocket.android.chatrooms.infrastructure.ChatRoomsRepository
import chat.rocket.android.server.infrastructure.ConnectionManager
import chat.rocket.android.util.livedata.transform
import chat.rocket.android.util.livedata.wrap
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.rest.spotlight
import chat.rocket.core.model.SpotlightResult
import com.shopify.livedataktx.distinct
import com.shopify.livedataktx.map
import com.shopify.livedataktx.nonNull
import kotlinx.coroutines.async
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.lang.IllegalArgumentException
import kotlin.coroutines.coroutineContext
class ChatRoomsViewModel(
private val connectionManager: ConnectionManager,
private val interactor: FetchChatRoomsInteractor,
private val repository: ChatRoomsRepository,
private val mapper: RoomUiModelMapper
) : ViewModel() {
private val query = MutableLiveData<Query>()
val loadingState = MutableLiveData<LoadingState>()
private val runContext = newSingleThreadContext("chat-rooms-view-model")
private val client = connectionManager.client
private var loaded = false
var showLastMessage = true
fun getChatRooms(): LiveData<RoomsModel> {
return Transformations.switchMap(query) { query ->
return@switchMap if (query.isSearch()) {
this@ChatRoomsViewModel.query.wrap(runContext) { _, data: MutableLiveData<RoomsModel> ->
val string = (query as Query.Search).query
// debounce, to not query while the user is writing
delay(200)
// TODO - find a better way for cancellation checking
if (!coroutineContext.isActive) return@wrap
val rooms = repository.search(string).let { mapper.map(it, showLastMessage = this.showLastMessage) }
data.postValue(rooms.toMutableList() + LoadingItemHolder())
if (!coroutineContext.isActive) return@wrap
val spotlight = spotlight(query.query)?.let { mapper.map(it, showLastMessage = this.showLastMessage) }
if (!coroutineContext.isActive) return@wrap
spotlight?.let {
data.postValue(rooms.toMutableList() + spotlight)
}.ifNull {
data.postValue(rooms)
}
}
} else {
repository.getChatRooms(query.asSortingOrder())
.nonNull()
.distinct()
.transform(runContext) { rooms ->
val mappedRooms = rooms?.let {
mapper.map(rooms, query.isGrouped(), this.showLastMessage)
}
if (loaded && mappedRooms?.isEmpty() == true) {
loadingState.postValue(LoadingState.Loaded(0))
}
mappedRooms
}
}
}
}
fun getUsersRoomListLocal(string: String): List<ItemHolder<*>> {
val rooms = GlobalScope.async(Dispatchers.IO) {
return@async repository.search(string).let { mapper.map(it, showLastMessage = showLastMessage) }
}
return runBlocking { rooms.await() }
}
fun getUsersRoomListSpotlight(string: String): List<ItemHolder<*>>? {
val rooms = GlobalScope.async(Dispatchers.IO) {
return@async spotlight(string)?.let { mapper.map(it, showLastMessage = showLastMessage) }
}
return runBlocking { rooms.await() }
}
private suspend fun spotlight(query: String): SpotlightResult? {
return try {
retryIO { client.spotlight(query) }
} catch (ex: Exception) {
ex.printStackTrace()
null
}
}
fun getStatus(): MutableLiveData<State> {
return connectionManager.stateLiveData.nonNull().distinct().map { state ->
if (state is State.Connected) fetchRooms()
state
}
}
private fun fetchRooms() {
GlobalScope.launch {
setLoadingState(LoadingState.Loading(repository.count()))
try {
interactor.refreshChatRooms()
setLoadingState(LoadingState.Loaded(repository.count()))
loaded = true
} catch (ex: Exception) {
Timber.d(ex, "Error refreshing chatrooms")
Timber.d(ex, "Message: $ex")
if (ex is RocketChatAuthException) {
setLoadingState(LoadingState.AuthError)
} else {
setLoadingState(LoadingState.Error(repository.count()))
}
}
}
}
fun setQuery(query: Query) {
this.query.value = query
}
private suspend fun setLoadingState(state: LoadingState) {
withContext(Dispatchers.Main) {
loadingState.value = state
}
}
}
typealias RoomsModel = List<ItemHolder<*>>
sealed class LoadingState {
data class Loading(val count: Long) : LoadingState()
data class Loaded(val count: Long) : LoadingState()
data class Error(val count: Long) : LoadingState()
object AuthError : LoadingState()
}
sealed class Query {
data class ByActivity(val grouped: Boolean = false, val unreadOnTop: Boolean = false) : Query()
data class ByName(val grouped: Boolean = false, val unreadOnTop: Boolean = false ) : Query()
data class Search(val query: String) : Query()
}
fun Query.isSearch(): Boolean = this is Query.Search
fun Query.isGrouped(): Boolean {
return when(this) {
is Query.Search -> false
is Query.ByName -> grouped
is Query.ByActivity -> grouped
}
}
fun Query.isUnreadOnTop(): Boolean {
return when(this) {
is Query.Search -> false
is Query.ByName -> unreadOnTop
is Query.ByActivity -> unreadOnTop
}
}
fun Query.asSortingOrder(): ChatRoomsRepository.Order {
return when(this) {
is Query.ByName -> {
if (grouped && !unreadOnTop) {
ChatRoomsRepository.Order.GROUPED_NAME
}
else if(unreadOnTop && !grouped){
ChatRoomsRepository.Order.UNREAD_ON_TOP_NAME
}
else if(unreadOnTop && grouped){
ChatRoomsRepository.Order.UNREAD_ON_TOP_GROUPED_NAME
}
else {
ChatRoomsRepository.Order.NAME
}
}
is Query.ByActivity -> {
if (grouped && !unreadOnTop) {
ChatRoomsRepository.Order.GROUPED_ACTIVITY
}
else if(unreadOnTop && !grouped){
ChatRoomsRepository.Order.UNREAD_ON_TOP_ACTIVITY
}
else if(unreadOnTop && grouped){
ChatRoomsRepository.Order.UNREAD_ON_TOP_GROUPED_ACTIVITY
}
else {
ChatRoomsRepository.Order.ACTIVITY
}
}
else -> throw IllegalArgumentException("Should be ByName or ByActivity")
}
}