Private
Public Access
1
0

More pleasant OOTB experience

This commit is contained in:
2024-04-10 23:35:54 -07:00
parent d474ce1c10
commit 6ed8e88bf0
7 changed files with 159 additions and 67 deletions

View File

@@ -20,6 +20,8 @@ import retrofit2.converter.gson.GsonConverterFactory
import java.net.URL
interface APIClient {
val isConfigured: Boolean
fun getAPIInterface(): APIInterface
fun getWebSocketClient(
serverPath: String,
@@ -99,11 +101,12 @@ class APIClientFactory {
companion object {
fun createClient(serverString: String?, authentication: Authentication?): APIClient {
if (serverString == null || authentication == null) {
return InvalidConfigurationAPIClient()
return InvalidConfigurationAPIClient(InvalidConfigurationAPIClient.Issue.NOT_CONFIGURED)
}
// 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)
}
@@ -111,10 +114,23 @@ class APIClientFactory {
}
// TODO: Is this a dumb idea?
class InvalidConfigurationAPIClient: APIClient {
private class InvalidConfigurationAPIInterface: APIInterface {
class InvalidConfigurationAPIClient(val issue: Issue): APIClient {
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 {
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()
@@ -124,16 +140,14 @@ class InvalidConfigurationAPIClient: APIClient {
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 authenticate(request: AuthenticationRequest): retrofit2.Response<AuthenticationResponse> = throwError()
override suspend fun getMessages(
conversationGUID: String,
limit: Int?,
beforeMessageGUID: GUID?,
afterMessageGUID: GUID?
): retrofit2.Response<List<Message>> = throwError()
override suspend fun getMessages(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 {
return InvalidConfigurationAPIInterface()
return InvalidConfigurationAPIInterface(issue)
}
override fun getWebSocketClient(
@@ -162,6 +176,9 @@ class RetrofitAPIClient(
.addConverterFactory(GsonConverterFactory.create())
.build()
override val isConfigured: Boolean
get() = true
override fun getAPIInterface(): APIInterface {
return retrofit.create(APIInterface::class.java)
}

View File

@@ -28,16 +28,12 @@ import java.io.InputStream
import java.util.Date
import java.util.UUID
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 CONVERSATION_MESSAGE_SYNC_COUNT = 10
class ChatRepository(
apiClient: APIClient,
private var apiClient: APIClient,
private val database: CachedChatDatabase,
) {
sealed class Error {
@@ -48,6 +44,11 @@ class ChatRepository(
override val title: String = "Connection Error"
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
@@ -71,6 +72,10 @@ class ChatRepository(
val errorEncounteredChannel: SharedFlow<Error>
get() = _errorEncounteredChannel.asSharedFlow()
val isConfigured: Boolean
get() = apiClient.isConfigured
// New messages for a particular conversation
fun messagesChanged(conversation: Conversation): Flow<List<Message>> =
database.messagesChanged(conversation)
@@ -99,6 +104,7 @@ class ChatRepository(
private var updateWatchScope: CoroutineScope? = null
fun updateAPIClient(client: APIClient) {
this.apiClient = client
this.apiInterface = client.getAPIInterface()
this.updateMonitor = UpdateMonitor(client)
@@ -160,7 +166,7 @@ class ChatRepository(
return database.fetchMessages(conversation)
}
suspend fun synchronize() = try {
suspend fun synchronize() = withErrorChannelHandling {
Log.d(REPO_LOG, "Synchronizing conversations")
// Sync conversations
@@ -173,26 +179,17 @@ class ChatRepository(
for (conversation in sortedConversations.take(CONVERSATION_MESSAGE_SYNC_COUNT)) {
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.
// But keep in mind that outgoing message GUIDs are fake...
val messages = fetchMessages(conversation, limit = limit)
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)
} 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 {
@@ -217,6 +214,18 @@ class ChatRepository(
// - 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> {
return apiInterface.getConversations().bodyOnSuccessOrThrow()
}
@@ -279,6 +288,7 @@ class ChatRepository(
}
} catch (e: java.lang.Exception) {
Log.e(REPO_LOG, "Error uploading attachment (${e.message}). Dropping...")
_errorEncounteredChannel.emit(Error.AttachmentError("Upload error: ${e.message}"))
}
try {

View File

@@ -40,6 +40,11 @@ class UpdateMonitor(private val client: APIClient) : WebSocketListener() {
private val _messageAdded: MutableSharedFlow<Message> = MutableSharedFlow()
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")
try {
this.webSocket = client.getWebSocketClient(