Private
Public Access
1
0

2 Commits

9 changed files with 67 additions and 28 deletions

View File

@@ -1,6 +1,5 @@
package net.buzzert.kordophonedroid.ui.attachments
import androidx.compose.foundation.Indication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
@@ -8,16 +7,15 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.AsyncImage
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import net.buzzert.kordophonedroid.ui.LocalNavController
import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar
@@ -41,12 +39,19 @@ fun AttachmentViewer(attachmentGuid: String) {
Column(modifier = Modifier.padding(padding)) {
Spacer(modifier = Modifier.weight(1f))
AsyncImage(
SubcomposeAsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(data)
.crossfade(true)
.build(),
contentDescription = "",
loading = {
Box {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
},
modifier = Modifier
.zoomable(zoomState)
.fillMaxWidth()

Binary file not shown.

Binary file not shown.

View File

@@ -97,6 +97,7 @@ class CachedChatDatabase (private val realmConfig: RealmConfiguration) {
date = conversation.date
unreadCount = conversation.unreadCount
lastMessagePreview = conversation.lastMessagePreview
lastMessageGUID = conversation.lastMessageGUID
}
} catch (e: NoSuchElementException) {
// Conversation does not exist. Copy it to the realm.
@@ -121,6 +122,10 @@ class CachedChatDatabase (private val realmConfig: RealmConfiguration) {
}
fun writeMessages(messages: List<ModelMessage>, conversation: ModelConversation, outgoing: Boolean = false) {
if (messages.isEmpty()) {
return
}
val dbConversation = getManagedConversationByGuid(conversation.guid)
realm.writeBlocking {
messages
@@ -128,8 +133,17 @@ class CachedChatDatabase (private val realmConfig: RealmConfiguration) {
.map { copyToRealm(it, updatePolicy = UpdatePolicy.ALL) }
findLatest(dbConversation)?.let {
it.lastMessagePreview = messages.last().displayText
it.date = messages.last().date.toInstant().toRealmInstant()
val lastMessage = messages.maxByOrNull { it.date }!!
val lastMessageDate = lastMessage.date.toInstant().toRealmInstant()
if (lastMessageDate > it.date) {
it.lastMessageGUID = lastMessage.guid
it.lastMessagePreview = lastMessage.displayText
// This will cause sort order to change. I think this ends
// up getting updated whenever we get conversation changes anyway.
// it.date = lastMessageDate
}
}
}
}

View File

@@ -1,17 +1,13 @@
package net.buzzert.kordophone.backend.db.model
import io.realm.kotlin.Realm
import io.realm.kotlin.ext.realmListOf
import io.realm.kotlin.ext.realmSetOf
import io.realm.kotlin.ext.toRealmList
import io.realm.kotlin.types.RealmInstant
import io.realm.kotlin.types.RealmList
import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.RealmSet
import io.realm.kotlin.types.annotations.PrimaryKey
import net.buzzert.kordophone.backend.model.GUID
import org.mongodb.kbson.ObjectId
import java.time.Instant
import net.buzzert.kordophone.backend.model.Conversation as ModelConversation
import java.util.Date
@@ -24,6 +20,7 @@ open class Conversation(
var date: RealmInstant,
var unreadCount: Int,
var lastMessageGUID: String?,
var lastMessagePreview: String?,
): RealmObject
{
@@ -35,10 +32,11 @@ open class Conversation(
date = RealmInstant.now(),
unreadCount = 0,
lastMessagePreview = null,
lastMessageGUID = null,
)
fun toConversation(): ModelConversation {
val conversation = ModelConversation(
return ModelConversation(
displayName = displayName,
participants = participants.toList(),
date = Date.from(date.toInstant()),
@@ -46,9 +44,8 @@ open class Conversation(
guid = guid,
lastMessagePreview = lastMessagePreview,
lastMessage = null,
lastFetchedMessageGUID = lastMessageGUID
)
return conversation
}
override fun equals(other: Any?): Boolean {
@@ -57,6 +54,10 @@ open class Conversation(
val o = other as Conversation
return guid == o.guid
}
override fun hashCode(): Int {
return guid.hashCode()
}
}
fun ModelConversation.toDatabaseConversation(): Conversation {
@@ -67,6 +68,7 @@ fun ModelConversation.toDatabaseConversation(): Conversation {
date = from.date.toInstant().toRealmInstant()
unreadCount = from.unreadCount
lastMessagePreview = from.lastMessagePreview
lastMessageGUID = from.lastFetchedMessageGUID
guid = from.guid
}
}

View File

@@ -27,6 +27,8 @@ data class Conversation(
@SerializedName("lastMessage")
var lastMessage: Message?,
var lastFetchedMessageGUID: String?,
) {
companion object {
fun generate(): Conversation {
@@ -38,6 +40,7 @@ data class Conversation(
unreadCount = 0,
lastMessagePreview = null,
lastMessage = null,
lastFetchedMessageGUID = null,
)
}
}
@@ -59,7 +62,7 @@ data class Conversation(
participants == o.participants &&
displayName == o.displayName &&
unreadCount == o.unreadCount &&
lastMessagePreview == o.lastMessagePreview
lastFetchedMessageGUID == o.lastFetchedMessageGUID
)
}
@@ -69,8 +72,8 @@ data class Conversation(
result = 31 * result + participants.hashCode()
result = 31 * result + (displayName?.hashCode() ?: 0)
result = 31 * result + unreadCount
result = 31 * result + (lastMessagePreview?.hashCode() ?: 0)
result = 31 * result + (lastMessage?.hashCode() ?: 0)
result = 31 * result + (lastFetchedMessageGUID?.hashCode() ?: 0)
return result
}
}

View File

@@ -182,9 +182,7 @@ class ChatRepository(
}
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)
val messages = fetchMessages(conversation, limit = limit, afterGUID = conversation.lastFetchedMessageGUID)
database.writeMessages(messages, conversation)
}
@@ -233,10 +231,10 @@ class ChatRepository(
private suspend fun fetchMessages(
conversation: Conversation,
limit: Int? = null,
before: Message? = null,
after: Message? = null,
beforeGUID: String? = null,
afterGUID: String? = null,
): List<Message> {
return apiInterface.getMessages(conversation.guid, limit, before?.guid, after?.guid)
return apiInterface.getMessages(conversation.guid, limit, beforeGUID, afterGUID)
.bodyOnSuccessOrThrow()
.onEach { it.conversation = conversation }
}

View File

@@ -9,6 +9,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import net.buzzert.kordophone.backend.db.CachedChatDatabase
import net.buzzert.kordophone.backend.model.Message
import net.buzzert.kordophone.backend.model.OutgoingMessage
import net.buzzert.kordophone.backend.server.APIClient
import net.buzzert.kordophone.backend.server.APIInterface
import net.buzzert.kordophone.backend.server.Authentication
@@ -85,13 +86,18 @@ class BackendTests {
val (repository, mockServer) = mockRepository()
val conversation = mockServer.addTestConversations(1).first()
val outgoingMessage = MockServer.generateMessage(conversation)
val generatedMessage = MockServer.generateMessage(conversation)
val outgoingMessage = OutgoingMessage(
body = generatedMessage.text,
conversation = conversation,
attachmentUris = setOf(),
attachmentDataSource = { null },
)
val guid = repository.enqueueOutgoingMessage(outgoingMessage, conversation)
repository.enqueueOutgoingMessage(outgoingMessage)
val event = repository.messageDeliveredChannel.first()
assertEquals(event.requestGuid, guid)
assertEquals(event.message.text, outgoingMessage.text)
assertEquals(event.message.text, outgoingMessage.body)
repository.close()
}

View File

@@ -18,18 +18,19 @@ import net.buzzert.kordophone.backend.server.AuthenticationRequest
import net.buzzert.kordophone.backend.server.AuthenticationResponse
import net.buzzert.kordophone.backend.server.SendMessageRequest
import net.buzzert.kordophone.backend.server.SendMessageResponse
import net.buzzert.kordophone.backend.server.UploadAttachmentResponse
import net.buzzert.kordophone.backend.server.authenticatedWebSocketURL
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import retrofit2.Call
import retrofit2.Response
import java.util.Date
import java.util.UUID
@@ -66,7 +67,8 @@ class MockServer {
unreadCount = 0,
lastMessagePreview = null,
lastMessage = null,
guid = UUID.randomUUID().toString()
guid = UUID.randomUUID().toString(),
lastFetchedMessageGUID = null,
)
}
}
@@ -166,6 +168,8 @@ class MockServerClient(private val server: MockServer): APIClient, WebSocketList
private var updateWatchJob: Job? = null
private val gson: Gson = Gson()
override val isConfigured: Boolean = true
override fun getAPIInterface(): APIInterface {
return MockServerInterface(server)
}
@@ -268,6 +272,13 @@ class MockServerInterface(private val server: MockServer): APIInterface {
TODO("Not yet implemented")
}
override suspend fun uploadAttachment(
filename: String,
body: RequestBody
): Response<UploadAttachmentResponse> {
TODO("Not yet implemented")
}
override suspend fun authenticate(request: AuthenticationRequest): Response<AuthenticationResponse> {
// Anything goes!
val response = AuthenticationResponse(