More pleasant OOTB experience
This commit is contained in:
@@ -2,12 +2,29 @@ package net.buzzert.kordophonedroid.ui.conversationlist
|
|||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.Divider
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
@@ -27,10 +44,11 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.buzzert.kordophone.backend.model.Conversation
|
import net.buzzert.kordophone.backend.model.Conversation
|
||||||
|
import net.buzzert.kordophonedroid.R
|
||||||
import net.buzzert.kordophonedroid.ui.Destination
|
import net.buzzert.kordophonedroid.ui.Destination
|
||||||
import net.buzzert.kordophonedroid.ui.LocalNavController
|
import net.buzzert.kordophonedroid.ui.LocalNavController
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
@@ -54,8 +72,12 @@ fun ConversationListScreen(
|
|||||||
viewModel: ConversationListViewModel = hiltViewModel(),
|
viewModel: ConversationListViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val conversations by viewModel.conversations.collectAsStateWithLifecycle(initialValue = emptyList())
|
val conversations by viewModel.conversations.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
val encounteredError by viewModel.encounteredConnectionError
|
||||||
|
|
||||||
ConversationListView(
|
ConversationListView(
|
||||||
conversations = conversations,
|
conversations = conversations,
|
||||||
|
isConfigured = viewModel.isServerConfigured,
|
||||||
|
encounteredError = encounteredError,
|
||||||
onRefresh = suspend { viewModel.refresh() }
|
onRefresh = suspend { viewModel.refresh() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -64,6 +86,8 @@ fun ConversationListScreen(
|
|||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
fun ConversationListView(
|
fun ConversationListView(
|
||||||
conversations: List<Conversation>,
|
conversations: List<Conversation>,
|
||||||
|
isConfigured: Boolean = true,
|
||||||
|
encounteredError: Boolean = false,
|
||||||
onRefresh: suspend () -> Unit,
|
onRefresh: suspend () -> Unit,
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
@@ -77,6 +101,8 @@ fun ConversationListView(
|
|||||||
refreshing = false
|
refreshing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val showErrorScreen = conversations.isEmpty() && encounteredError
|
||||||
|
|
||||||
val refreshState = rememberPullRefreshState(refreshing = refreshing, onRefresh = ::refresh)
|
val refreshState = rememberPullRefreshState(refreshing = refreshing, onRefresh = ::refresh)
|
||||||
|
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
@@ -90,35 +116,49 @@ fun ConversationListView(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Box(Modifier.pullRefresh(refreshState)) {
|
if (showErrorScreen) {
|
||||||
LazyColumn(
|
NoContentView(
|
||||||
state = listState,
|
icon = R.drawable.error,
|
||||||
modifier = Modifier
|
text = "Connection error",
|
||||||
.padding(it)
|
onSettings = onSettingsInvoked
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
items(conversations) { conversation ->
|
|
||||||
val clickHandler = {
|
|
||||||
val route = Destination.MessageList.createRoute(conversation.guid)
|
|
||||||
navController.navigate(route)
|
|
||||||
}
|
|
||||||
|
|
||||||
ConversationListItem(
|
|
||||||
name = conversation.formattedDisplayName(),
|
|
||||||
id = conversation.guid,
|
|
||||||
isUnread = conversation.unreadCount > 0,
|
|
||||||
lastMessagePreview = conversation.lastMessagePreview ?: "",
|
|
||||||
date = conversation.date,
|
|
||||||
onClick = clickHandler
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PullRefreshIndicator(
|
|
||||||
refreshing = refreshing,
|
|
||||||
state = refreshState,
|
|
||||||
modifier = Modifier.align(Alignment.TopCenter),
|
|
||||||
)
|
)
|
||||||
|
} else if (!isConfigured) {
|
||||||
|
NoContentView(
|
||||||
|
icon = R.drawable.storage,
|
||||||
|
text = "Server not configured",
|
||||||
|
onSettings = onSettingsInvoked
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(Modifier.pullRefresh(refreshState)) {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(it)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
items(conversations) { conversation ->
|
||||||
|
val clickHandler = {
|
||||||
|
val route = Destination.MessageList.createRoute(conversation.guid)
|
||||||
|
navController.navigate(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
ConversationListItem(
|
||||||
|
name = conversation.formattedDisplayName(),
|
||||||
|
id = conversation.guid,
|
||||||
|
isUnread = conversation.unreadCount > 0,
|
||||||
|
lastMessagePreview = conversation.lastMessagePreview ?: "",
|
||||||
|
date = conversation.date,
|
||||||
|
onClick = clickHandler
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PullRefreshIndicator(
|
||||||
|
refreshing = refreshing,
|
||||||
|
state = refreshState,
|
||||||
|
modifier = Modifier.align(Alignment.TopCenter),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,22 @@
|
|||||||
package net.buzzert.kordophonedroid.ui.conversationlist
|
package net.buzzert.kordophonedroid.ui.conversationlist
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.cache
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import net.buzzert.kordophone.backend.model.Conversation
|
import net.buzzert.kordophone.backend.model.Conversation
|
||||||
import net.buzzert.kordophone.backend.server.APIClientFactory
|
import net.buzzert.kordophone.backend.server.APIClientFactory
|
||||||
import net.buzzert.kordophone.backend.server.Authentication
|
|
||||||
import net.buzzert.kordophone.backend.server.ChatRepository
|
import net.buzzert.kordophone.backend.server.ChatRepository
|
||||||
import net.buzzert.kordophone.backend.server.RetrofitAPIClient
|
|
||||||
import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository
|
import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import java.net.URL
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
const val CL_VM_LOG: String = "ConversationListViewModel"
|
const val CL_VM_LOG: String = "ConversationListViewModel"
|
||||||
@@ -38,6 +34,14 @@ class ConversationListViewModel @Inject constructor(
|
|||||||
.reversed()
|
.reversed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isServerConfigured: Boolean
|
||||||
|
get() = chatRepository.isConfigured
|
||||||
|
|
||||||
|
val encounteredConnectionError: State<Boolean>
|
||||||
|
get() = _encounteredConnectionError
|
||||||
|
|
||||||
|
private val _encounteredConnectionError = mutableStateOf(false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Watch for config changes
|
// Watch for config changes
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -56,6 +60,12 @@ class ConversationListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
chatRepository.errorEncounteredChannel.collect {
|
||||||
|
_encounteredConnectionError.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun refresh() {
|
suspend fun refresh() {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ fun NoContentView(
|
|||||||
@DrawableRes icon: Int,
|
@DrawableRes icon: Int,
|
||||||
text: String,
|
text: String,
|
||||||
onSettings: () -> Unit,
|
onSettings: () -> Unit,
|
||||||
modifier: Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|||||||
10
app/src/main/res/drawable/error.xml
Normal file
10
app/src/main/res/drawable/error.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M480,680Q497,680 508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680ZM440,520L520,520L520,280L440,280L440,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||||
|
</vector>
|
||||||
@@ -20,6 +20,8 @@ import retrofit2.converter.gson.GsonConverterFactory
|
|||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
interface APIClient {
|
interface APIClient {
|
||||||
|
val isConfigured: Boolean
|
||||||
|
|
||||||
fun getAPIInterface(): APIInterface
|
fun getAPIInterface(): APIInterface
|
||||||
fun getWebSocketClient(
|
fun getWebSocketClient(
|
||||||
serverPath: String,
|
serverPath: String,
|
||||||
@@ -99,11 +101,12 @@ class APIClientFactory {
|
|||||||
companion object {
|
companion object {
|
||||||
fun createClient(serverString: String?, authentication: Authentication?): APIClient {
|
fun createClient(serverString: String?, authentication: Authentication?): APIClient {
|
||||||
if (serverString == null || authentication == null) {
|
if (serverString == null || authentication == null) {
|
||||||
return InvalidConfigurationAPIClient()
|
return InvalidConfigurationAPIClient(InvalidConfigurationAPIClient.Issue.NOT_CONFIGURED)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to parse server string
|
// Try to parse server string
|
||||||
val serverURL = HttpUrl.parse(serverString) ?: return InvalidConfigurationAPIClient()
|
val serverURL = HttpUrl.parse(serverString)
|
||||||
|
?: return InvalidConfigurationAPIClient(InvalidConfigurationAPIClient.Issue.INVALID_HOST_URL)
|
||||||
|
|
||||||
return RetrofitAPIClient(serverURL.url(), authentication)
|
return RetrofitAPIClient(serverURL.url(), authentication)
|
||||||
}
|
}
|
||||||
@@ -111,10 +114,23 @@ class APIClientFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Is this a dumb idea?
|
// TODO: Is this a dumb idea?
|
||||||
class InvalidConfigurationAPIClient: APIClient {
|
class InvalidConfigurationAPIClient(val issue: Issue): APIClient {
|
||||||
private class InvalidConfigurationAPIInterface: APIInterface {
|
enum class Issue {
|
||||||
|
NOT_CONFIGURED,
|
||||||
|
INVALID_CONFIGURATION,
|
||||||
|
INVALID_HOST_URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotConfiguredError: Throwable(message = "Not configured.")
|
||||||
|
class InvalidConfigurationError(submessage: String): Throwable(message = "Invalid configuration: $submessage")
|
||||||
|
|
||||||
|
private class InvalidConfigurationAPIInterface(val issue: Issue): APIInterface {
|
||||||
private fun throwError(): Nothing {
|
private fun throwError(): Nothing {
|
||||||
throw Error("Invalid Configuration.")
|
when (issue) {
|
||||||
|
Issue.NOT_CONFIGURED -> throw NotConfiguredError()
|
||||||
|
Issue.INVALID_CONFIGURATION -> throw InvalidConfigurationError("Unknown.")
|
||||||
|
Issue.INVALID_HOST_URL -> throw InvalidConfigurationError("Invalid host URL.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getVersion(): ResponseBody = throwError()
|
override suspend fun getVersion(): ResponseBody = throwError()
|
||||||
@@ -124,16 +140,14 @@ class InvalidConfigurationAPIClient: APIClient {
|
|||||||
override suspend fun fetchAttachment(guid: String, preview: Boolean): ResponseBody = throwError()
|
override suspend fun fetchAttachment(guid: String, preview: Boolean): ResponseBody = throwError()
|
||||||
override suspend fun uploadAttachment(filename: String, body: RequestBody): retrofit2.Response<UploadAttachmentResponse> = throwError()
|
override suspend fun uploadAttachment(filename: String, body: RequestBody): retrofit2.Response<UploadAttachmentResponse> = throwError()
|
||||||
override suspend fun authenticate(request: AuthenticationRequest): retrofit2.Response<AuthenticationResponse> = throwError()
|
override suspend fun authenticate(request: AuthenticationRequest): retrofit2.Response<AuthenticationResponse> = throwError()
|
||||||
override suspend fun getMessages(
|
override suspend fun getMessages(conversationGUID: String, limit: Int?, beforeMessageGUID: GUID?, afterMessageGUID: GUID?): retrofit2.Response<List<Message>> = throwError()
|
||||||
conversationGUID: String,
|
|
||||||
limit: Int?,
|
|
||||||
beforeMessageGUID: GUID?,
|
|
||||||
afterMessageGUID: GUID?
|
|
||||||
): retrofit2.Response<List<Message>> = throwError()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val isConfigured: Boolean
|
||||||
|
get() { return issue != Issue.NOT_CONFIGURED }
|
||||||
|
|
||||||
override fun getAPIInterface(): APIInterface {
|
override fun getAPIInterface(): APIInterface {
|
||||||
return InvalidConfigurationAPIInterface()
|
return InvalidConfigurationAPIInterface(issue)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getWebSocketClient(
|
override fun getWebSocketClient(
|
||||||
@@ -162,6 +176,9 @@ class RetrofitAPIClient(
|
|||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
override val isConfigured: Boolean
|
||||||
|
get() = true
|
||||||
|
|
||||||
override fun getAPIInterface(): APIInterface {
|
override fun getAPIInterface(): APIInterface {
|
||||||
return retrofit.create(APIInterface::class.java)
|
return retrofit.create(APIInterface::class.java)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,16 +28,12 @@ import java.io.InputStream
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.ArrayBlockingQueue
|
import java.util.concurrent.ArrayBlockingQueue
|
||||||
import kotlin.Boolean
|
|
||||||
import kotlin.Int
|
|
||||||
import kotlin.String
|
|
||||||
import kotlin.let
|
|
||||||
|
|
||||||
const val REPO_LOG: String = "ChatRepository"
|
const val REPO_LOG: String = "ChatRepository"
|
||||||
const val CONVERSATION_MESSAGE_SYNC_COUNT = 10
|
const val CONVERSATION_MESSAGE_SYNC_COUNT = 10
|
||||||
|
|
||||||
class ChatRepository(
|
class ChatRepository(
|
||||||
apiClient: APIClient,
|
private var apiClient: APIClient,
|
||||||
private val database: CachedChatDatabase,
|
private val database: CachedChatDatabase,
|
||||||
) {
|
) {
|
||||||
sealed class Error {
|
sealed class Error {
|
||||||
@@ -48,6 +44,11 @@ class ChatRepository(
|
|||||||
override val title: String = "Connection Error"
|
override val title: String = "Connection Error"
|
||||||
override val description: String = message ?: "???"
|
override val description: String = message ?: "???"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class AttachmentError(val message: String): Error() {
|
||||||
|
override val title: String = "Attachment Error"
|
||||||
|
override val description: String = message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// All (Cached) Conversations
|
// All (Cached) Conversations
|
||||||
@@ -71,6 +72,10 @@ class ChatRepository(
|
|||||||
val errorEncounteredChannel: SharedFlow<Error>
|
val errorEncounteredChannel: SharedFlow<Error>
|
||||||
get() = _errorEncounteredChannel.asSharedFlow()
|
get() = _errorEncounteredChannel.asSharedFlow()
|
||||||
|
|
||||||
|
val isConfigured: Boolean
|
||||||
|
get() = apiClient.isConfigured
|
||||||
|
|
||||||
|
// New messages for a particular conversation
|
||||||
fun messagesChanged(conversation: Conversation): Flow<List<Message>> =
|
fun messagesChanged(conversation: Conversation): Flow<List<Message>> =
|
||||||
database.messagesChanged(conversation)
|
database.messagesChanged(conversation)
|
||||||
|
|
||||||
@@ -99,6 +104,7 @@ class ChatRepository(
|
|||||||
private var updateWatchScope: CoroutineScope? = null
|
private var updateWatchScope: CoroutineScope? = null
|
||||||
|
|
||||||
fun updateAPIClient(client: APIClient) {
|
fun updateAPIClient(client: APIClient) {
|
||||||
|
this.apiClient = client
|
||||||
this.apiInterface = client.getAPIInterface()
|
this.apiInterface = client.getAPIInterface()
|
||||||
this.updateMonitor = UpdateMonitor(client)
|
this.updateMonitor = UpdateMonitor(client)
|
||||||
|
|
||||||
@@ -160,7 +166,7 @@ class ChatRepository(
|
|||||||
return database.fetchMessages(conversation)
|
return database.fetchMessages(conversation)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun synchronize() = try {
|
suspend fun synchronize() = withErrorChannelHandling {
|
||||||
Log.d(REPO_LOG, "Synchronizing conversations")
|
Log.d(REPO_LOG, "Synchronizing conversations")
|
||||||
|
|
||||||
// Sync conversations
|
// Sync conversations
|
||||||
@@ -173,26 +179,17 @@ class ChatRepository(
|
|||||||
for (conversation in sortedConversations.take(CONVERSATION_MESSAGE_SYNC_COUNT)) {
|
for (conversation in sortedConversations.take(CONVERSATION_MESSAGE_SYNC_COUNT)) {
|
||||||
synchronizeConversation(conversation)
|
synchronizeConversation(conversation)
|
||||||
}
|
}
|
||||||
} catch (e: java.lang.Exception) {
|
|
||||||
_errorEncounteredChannel.emit(Error.ConnectionError(e.message))
|
|
||||||
} catch (e: java.lang.Error) {
|
|
||||||
_errorEncounteredChannel.emit(Error.ConnectionError(e.message))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun synchronizeConversation(conversation: Conversation, limit: Int = 15) = try {
|
suspend fun synchronizeConversation(conversation: Conversation, limit: Int = 15) = withErrorChannelHandling {
|
||||||
// TODO: Should only fetch messages after the last GUID we know about.
|
// TODO: Should only fetch messages after the last GUID we know about.
|
||||||
// But keep in mind that outgoing message GUIDs are fake...
|
// But keep in mind that outgoing message GUIDs are fake...
|
||||||
val messages = fetchMessages(conversation, limit = limit)
|
val messages = fetchMessages(conversation, limit = limit)
|
||||||
database.writeMessages(messages, conversation)
|
database.writeMessages(messages, conversation)
|
||||||
} catch (e: java.lang.Exception) {
|
|
||||||
_errorEncounteredChannel.emit(Error.ConnectionError(e.message))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun markConversationAsRead(conversation: Conversation) = try {
|
suspend fun markConversationAsRead(conversation: Conversation) = withErrorChannelHandling(silent = true) {
|
||||||
apiInterface.markConversation(conversation.guid)
|
apiInterface.markConversation(conversation.guid)
|
||||||
} catch (e: java.lang.Exception) {
|
|
||||||
// Don't report via the channel, but log it.
|
|
||||||
Log.e(REPO_LOG, "Error marking conversation as read: ${e.message}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchAttachmentDataSource(guid: String, preview: Boolean): BufferedSource {
|
suspend fun fetchAttachmentDataSource(guid: String, preview: Boolean): BufferedSource {
|
||||||
@@ -217,6 +214,18 @@ class ChatRepository(
|
|||||||
|
|
||||||
// - private
|
// - private
|
||||||
|
|
||||||
|
private suspend fun withErrorChannelHandling(silent: Boolean = false, body: suspend () -> Unit) {
|
||||||
|
try {
|
||||||
|
body()
|
||||||
|
} catch (e: InvalidConfigurationAPIClient.NotConfiguredError) {
|
||||||
|
// Not configured yet: ignore.
|
||||||
|
} catch (e: java.lang.Exception) {
|
||||||
|
if (!silent) _errorEncounteredChannel.emit(Error.ConnectionError(e.message))
|
||||||
|
} catch (e: java.lang.Error) {
|
||||||
|
if (!silent) _errorEncounteredChannel.emit(Error.ConnectionError(e.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun fetchConversations(): List<Conversation> {
|
private suspend fun fetchConversations(): List<Conversation> {
|
||||||
return apiInterface.getConversations().bodyOnSuccessOrThrow()
|
return apiInterface.getConversations().bodyOnSuccessOrThrow()
|
||||||
}
|
}
|
||||||
@@ -279,6 +288,7 @@ class ChatRepository(
|
|||||||
}
|
}
|
||||||
} catch (e: java.lang.Exception) {
|
} catch (e: java.lang.Exception) {
|
||||||
Log.e(REPO_LOG, "Error uploading attachment (${e.message}). Dropping...")
|
Log.e(REPO_LOG, "Error uploading attachment (${e.message}). Dropping...")
|
||||||
|
_errorEncounteredChannel.emit(Error.AttachmentError("Upload error: ${e.message}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ class UpdateMonitor(private val client: APIClient) : WebSocketListener() {
|
|||||||
private val _messageAdded: MutableSharedFlow<Message> = MutableSharedFlow()
|
private val _messageAdded: MutableSharedFlow<Message> = MutableSharedFlow()
|
||||||
|
|
||||||
fun beginMonitoringUpdates() {
|
fun beginMonitoringUpdates() {
|
||||||
|
if (!client.isConfigured) {
|
||||||
|
Log.e(UPMON_LOG, "Closing websocket connection because client is not configured.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Log.d(UPMON_LOG, "Opening websocket connection")
|
Log.d(UPMON_LOG, "Opening websocket connection")
|
||||||
try {
|
try {
|
||||||
this.webSocket = client.getWebSocketClient(
|
this.webSocket = client.getWebSocketClient(
|
||||||
|
|||||||
Reference in New Issue
Block a user