diff --git a/backend/chat-cache-test b/backend/chat-cache-test index ccbf1ed..650c929 100644 Binary files a/backend/chat-cache-test and b/backend/chat-cache-test differ diff --git a/backend/chat-cache-test.lock b/backend/chat-cache-test.lock index 4b8f81e..ccc2fcb 100644 Binary files a/backend/chat-cache-test.lock and b/backend/chat-cache-test.lock differ diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/db/CachedChatDatabase.kt b/backend/src/main/java/net/buzzert/kordophone/backend/db/CachedChatDatabase.kt index bff0c62..4beb1e6 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/db/CachedChatDatabase.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/db/CachedChatDatabase.kt @@ -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, 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 + } } } } diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Conversation.kt b/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Conversation.kt index 0c455db..0cf3dfb 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Conversation.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Conversation.kt @@ -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 } } diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/model/Conversation.kt b/backend/src/main/java/net/buzzert/kordophone/backend/model/Conversation.kt index 2ed7bfe..3af38a4 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/model/Conversation.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/model/Conversation.kt @@ -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 } } diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt b/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt index 6d06522..4181c84 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt @@ -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 { - return apiInterface.getMessages(conversation.guid, limit, before?.guid, after?.guid) + return apiInterface.getMessages(conversation.guid, limit, beforeGUID, afterGUID) .bodyOnSuccessOrThrow() .onEach { it.conversation = conversation } } diff --git a/backend/src/test/java/net/buzzert/kordophone/backend/BackendTests.kt b/backend/src/test/java/net/buzzert/kordophone/backend/BackendTests.kt index 3d294c6..d4cb2cb 100644 --- a/backend/src/test/java/net/buzzert/kordophone/backend/BackendTests.kt +++ b/backend/src/test/java/net/buzzert/kordophone/backend/BackendTests.kt @@ -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() } diff --git a/backend/src/test/java/net/buzzert/kordophone/backend/MockServer.kt b/backend/src/test/java/net/buzzert/kordophone/backend/MockServer.kt index 782a0b0..38230af 100644 --- a/backend/src/test/java/net/buzzert/kordophone/backend/MockServer.kt +++ b/backend/src/test/java/net/buzzert/kordophone/backend/MockServer.kt @@ -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 { + TODO("Not yet implemented") + } + override suspend fun authenticate(request: AuthenticationRequest): Response { // Anything goes! val response = AuthenticationResponse(