From b04a9c49cfd1507cca51986ee01ef94ff44c190b Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 3 Jul 2025 15:26:02 +0200 Subject: [PATCH] add chatBlock handling for threads Signed-off-by: Marcel Hibbe --- .../data/database/dao/ChatBlocksDaoTest.kt | 18 +++- .../data/database/dao/ChatMessagesDaoTest.kt | 9 +- .../messages/IncomingTextMessageViewHolder.kt | 6 +- .../OutcomingTextMessageViewHolder.kt | 6 +- .../com/nextcloud/talk/chat/ChatActivity.kt | 7 +- .../talk/chat/data/model/ChatMessage.kt | 6 +- .../network/OfflineFirstChatRepository.kt | 98 +++++++++++-------- .../talk/data/database/dao/ChatBlocksDao.kt | 43 +++++--- .../talk/data/database/dao/ChatMessagesDao.kt | 64 ++++++++---- .../database/mappers/ChatMessageMapUtils.kt | 15 +-- .../data/database/model/ChatBlockEntity.kt | 2 +- .../data/database/model/ChatMessageEntity.kt | 3 +- .../data/database/model/ConversationEntity.kt | 2 - .../talk/data/source/local/Migrations.kt | 12 +-- .../talk/models/domain/ConversationModel.kt | 1 - .../talk/models/json/chat/ChatMessageJson.kt | 9 +- .../models/json/conversations/Conversation.kt | 3 - .../reactions/ReactionsRepositoryImpl.kt | 5 +- .../utils/preview/ComposePreviewUtilsDaos.kt | 41 +++++--- 19 files changed, 229 insertions(+), 121 deletions(-) diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt index 8e077f863..e296c8f18 100644 --- a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt +++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt @@ -52,7 +52,8 @@ class ChatBlocksDaoTest { @Test fun testGetConnectedChatBlocks() = runTest { - usersDao.saveUser(createUserEntity("account1", "Account 1")) + val user = createUserEntity("account1", "Account 1") + usersDao.saveUser(user) val account1 = usersDao.getUserWithUserId("account1").blockingGet() conversationsDao.upsertConversations( @@ -77,6 +78,7 @@ class ChatBlocksDaoTest { internalConversationId = conversation1.internalId, accountId = conversation1.accountId, token = conversation1.token, + threadId = null, oldestMessageId = 50, newestMessageId = 60, hasHistory = true @@ -86,6 +88,7 @@ class ChatBlocksDaoTest { internalConversationId = conversation1.internalId, accountId = conversation1.accountId, token = conversation1.token, + threadId = null, oldestMessageId = 10, newestMessageId = 20, hasHistory = true @@ -95,6 +98,7 @@ class ChatBlocksDaoTest { internalConversationId = conversation1.internalId, accountId = conversation1.accountId, token = conversation1.token, + threadId = null, oldestMessageId = 45, newestMessageId = 55, hasHistory = true @@ -104,6 +108,7 @@ class ChatBlocksDaoTest { internalConversationId = conversation1.internalId, accountId = conversation1.accountId, token = conversation1.token, + threadId = null, oldestMessageId = 52, newestMessageId = 58, hasHistory = true @@ -113,6 +118,7 @@ class ChatBlocksDaoTest { internalConversationId = conversation1.internalId, accountId = conversation1.accountId, token = conversation1.token, + threadId = null, oldestMessageId = 1, newestMessageId = 99, hasHistory = true @@ -122,6 +128,7 @@ class ChatBlocksDaoTest { internalConversationId = conversation1.internalId, accountId = conversation1.accountId, token = conversation1.token, + threadId = null, oldestMessageId = 59, newestMessageId = 70, hasHistory = true @@ -131,6 +138,7 @@ class ChatBlocksDaoTest { internalConversationId = conversation1.internalId, accountId = conversation1.accountId, token = conversation1.token, + threadId = null, oldestMessageId = 80, newestMessageId = 90, hasHistory = true @@ -140,6 +148,7 @@ class ChatBlocksDaoTest { internalConversationId = conversation2.internalId, accountId = conversation2.accountId, token = conversation2.token, + threadId = null, oldestMessageId = 53, newestMessageId = 57, hasHistory = true @@ -156,9 +165,10 @@ class ChatBlocksDaoTest { chatBlocksDao.upsertChatBlock(chatBlockWithinButOtherConversation) val results = chatBlocksDao.getConnectedChatBlocks( - conversation1.internalId, - searchedChatBlock.oldestMessageId, - searchedChatBlock.newestMessageId + internalConversationId = conversation1.internalId, + threadId = null, + oldestMessageId = searchedChatBlock.oldestMessageId, + newestMessageId = searchedChatBlock.newestMessageId ) assertEquals(5, results.first().size) diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt index a6f8386e7..424b932b0 100644 --- a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt +++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt @@ -140,7 +140,11 @@ class ChatMessagesDaoTest { assertEquals("are", conv1chatMessage3.message) val chatMessagesConv1Since = - chatMessagesDao.getMessagesForConversationSince(conversation1.internalId, conv1chatMessage3.id) + chatMessagesDao.getMessagesForConversationSince( + conversation1.internalId, + conv1chatMessage3.id, + null + ) assertEquals(3, chatMessagesConv1Since.first().size) assertEquals("are", chatMessagesConv1Since.first()[0].message) assertEquals("some", chatMessagesConv1Since.first()[1].message) @@ -150,7 +154,8 @@ class ChatMessagesDaoTest { chatMessagesDao.getMessagesForConversationBeforeAndEqual( conversation1.internalId, conv1chatMessage3.id, - 3 + 3, + null ) assertEquals(3, chatMessagesConv1To.first().size) assertEquals("hello", chatMessagesConv1To.first()[2].message) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt index 7e020afbb..30cbe77c8 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt @@ -10,6 +10,7 @@ package com.nextcloud.talk.adapters.messages import android.content.Context +import android.text.SpannableStringBuilder import android.util.Log import android.util.TypedValue import android.view.View @@ -145,7 +146,10 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : binding.messageAuthor.visibility = View.GONE } binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) - binding.messageText.text = processedMessageText + // binding.messageText.text = processedMessageText + // just for debugging: + binding.messageText.text = + SpannableStringBuilder(processedMessageText).append(" (" + message.jsonMessageId + ")") } else { binding.messageText.visibility = View.GONE binding.checkboxContainer.visibility = View.VISIBLE diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt index 7510caf76..dcd08af6b 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt @@ -10,6 +10,7 @@ package com.nextcloud.talk.adapters.messages import android.content.Context +import android.text.SpannableStringBuilder import android.util.Log import android.util.TypedValue import android.view.View @@ -159,7 +160,10 @@ class OutcomingTextMessageViewHolder(itemView: View) : binding.messageTime.layoutParams = layoutParams viewThemeUtils.platform.colorTextView(binding.messageText, ColorRole.ON_SURFACE_VARIANT) - binding.messageText.text = processedMessageText + // binding.messageText.text = processedMessageText + // just for debugging: + binding.messageText.text = + SpannableStringBuilder(processedMessageText).append(" (" + message.jsonMessageId + ")") } else { binding.messageText.visibility = View.GONE binding.checkboxContainer.visibility = View.VISIBLE diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index e49c6fb44..b143de18f 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -557,7 +557,12 @@ class ChatActivity : val extras: Bundle? = intent.extras roomToken = extras?.getString(KEY_ROOM_TOKEN).orEmpty() - threadId = extras?.getLong(KEY_THREAD_ID) + + threadId = if (extras?.containsKey(KEY_THREAD_ID) == true) { + extras.getLong(KEY_THREAD_ID) + } else { + null + } sharedText = extras?.getString(BundleKeys.KEY_SHARED_TEXT).orEmpty() diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt index 84dfadd51..967583b98 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt @@ -45,9 +45,11 @@ data class ChatMessage( var token: String? = null, - var topmostParentId: Long? = null, + var threadId: Long? = null, - var childrenCount: Long? = 0, + var isThread: Boolean = false, + + // var childrenCount: Long? = 0, // guests or users var actorType: String? = null, diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt index 3ee3589aa..a8294c1fe 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt @@ -122,16 +122,10 @@ class OfflineFirstChatRepository @Inject constructor( private var threadId: Long? = null override fun initData(credentials: String, urlForChatting: String, roomToken: String, threadId: Long?) { - val threadIdAppendedString = if (threadId != null && threadId > 0) { - "@$threadId" - } else { - "" - } - internalConversationId = currentUser.id.toString() + "@" + roomToken + threadIdAppendedString + internalConversationId = currentUser.id.toString() + "@" + roomToken this.credentials = credentials this.urlForChatting = urlForChatting - this.threadId = threadId // use this threadId in API requests when fetching messages? + - // Introduce ChatBlocks for threads + this.threadId = threadId } override fun updateConversation(conversationModel: ConversationModel) { @@ -151,7 +145,8 @@ class OfflineFirstChatRepository @Inject constructor( Log.d(TAG, "conversationModel.internalId: " + conversationModel.internalId) Log.d(TAG, "conversationModel.lastReadMessage:" + conversationModel.lastReadMessage) - var newestMessageIdFromDb = chatDao.getNewestMessageId(internalConversationId) + // var newestMessageIdFromDb = chatDao.getNewestMessageId(internalConversationId, threadId) + var newestMessageIdFromDb = chatBlocksDao.getNewestMessageIdFromChatBlocks(internalConversationId, threadId) Log.d(TAG, "newestMessageIdFromDb: $newestMessageIdFromDb") val weAlreadyHaveSomeOfflineMessages = newestMessageIdFromDb > 0 @@ -197,7 +192,7 @@ class OfflineFirstChatRepository @Inject constructor( Log.e(TAG, "initial loading of messages failed") } - newestMessageIdFromDb = chatDao.getNewestMessageId(internalConversationId) + newestMessageIdFromDb = chatBlocksDao.getNewestMessageIdFromChatBlocks(internalConversationId, threadId) Log.d(TAG, "newestMessageIdFromDb after sync: $newestMessageIdFromDb") } @@ -211,9 +206,9 @@ class OfflineFirstChatRepository @Inject constructor( val limit = getCappedMessagesAmountOfChatBlock(newestMessageIdFromDb) val list = getMessagesBeforeAndEqual( - newestMessageIdFromDb, - internalConversationId, - limit + messageId = newestMessageIdFromDb, + internalConversationId = internalConversationId, + messageLimit = limit ) if (list.isNotEmpty()) { handleNewAndTempMessages( @@ -242,7 +237,8 @@ class OfflineFirstChatRepository @Inject constructor( val amountBetween = chatDao.getCountBetweenMessageIds( internalConversationId, messageId, - chatBlock.oldestMessageId + chatBlock.oldestMessageId, + threadId ) Log.d(TAG, "amount of messages between newestMessageId and oldest message of same ChatBlock:$amountBetween") @@ -292,7 +288,7 @@ class OfflineFirstChatRepository @Inject constructor( ) withNetworkParams.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap) - val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId) + val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId, DEFAULT_MESSAGES_LIMIT) if (loadFromServer) { Log.d(TAG, "Starting online request for loadMoreMessages") @@ -354,7 +350,10 @@ class OfflineFirstChatRepository @Inject constructor( updateUiForLastCommonRead() - val newestMessage = chatDao.getNewestMessageId(internalConversationId).toInt() + val newestMessage = chatBlocksDao.getNewestMessageIdFromChatBlocks( + internalConversationId, + threadId + ).toInt() // update field map vars for next cycle fieldMap = getFieldMap( @@ -380,7 +379,7 @@ class OfflineFirstChatRepository @Inject constructor( } // remove all temp messages from UI - val oldTempMessages = chatDao.getTempMessagesForConversation(internalConversationId) + val oldTempMessages = chatDao.getTempMessagesForConversation(internalConversationId, threadId) .first() .map(ChatMessageEntity::asModel) oldTempMessages.forEach { @@ -404,7 +403,7 @@ class OfflineFirstChatRepository @Inject constructor( ) // add the remaining temp messages to UI again - val remainingTempMessages = chatDao.getTempMessagesForConversation(internalConversationId) + val remainingTempMessages = chatDao.getTempMessagesForConversation(internalConversationId, threadId) .first() .sortedBy { it.internalId } .map(ChatMessageEntity::asModel) @@ -417,7 +416,7 @@ class OfflineFirstChatRepository @Inject constructor( _messageFlow.emit(triple) } - private suspend fun hasToLoadPreviousMessagesFromServer(beforeMessageId: Long): Boolean { + private suspend fun hasToLoadPreviousMessagesFromServer(beforeMessageId: Long, amountToCheck: Int): Boolean { val loadFromServer: Boolean val blockForMessage = getBlockOfMessage(beforeMessageId.toInt()) @@ -429,21 +428,19 @@ class OfflineFirstChatRepository @Inject constructor( Log.d(TAG, "The last chatBlock is reached so we won't request server for older messages") loadFromServer = false } else { - // we know that beforeMessageId and blockForMessage.oldestMessageId are in the same block. - // As we want the last DEFAULT_MESSAGES_LIMIT entries before beforeMessageId, we calculate if these - // messages are DEFAULT_MESSAGES_LIMIT entries apart from each other - val amountBetween = chatDao.getCountBetweenMessageIds( internalConversationId, beforeMessageId, - blockForMessage.oldestMessageId + blockForMessage.oldestMessageId, + threadId ) - loadFromServer = amountBetween < DEFAULT_MESSAGES_LIMIT + loadFromServer = amountBetween < amountToCheck Log.d( TAG, "Amount between messageId " + beforeMessageId + " and " + blockForMessage.oldestMessageId + - " is: " + amountBetween + " so 'loadFromServer' is " + loadFromServer + " is: " + amountBetween + " and $amountToCheck were needed, so 'loadFromServer' is " + + loadFromServer ) } return loadFromServer @@ -479,7 +476,7 @@ class OfflineFirstChatRepository @Inject constructor( override suspend fun getMessage(messageId: Long, bundle: Bundle): Flow { Log.d(TAG, "Get message with id $messageId") - val loadFromServer = hasToLoadPreviousMessagesFromServer(messageId) + val loadFromServer = hasToLoadPreviousMessagesFromServer(messageId, 1) if (loadFromServer) { val fieldMap = getFieldMap( @@ -495,8 +492,10 @@ class OfflineFirstChatRepository @Inject constructor( Log.d(TAG, "Starting online request for single message (e.g. a reply)") sync(bundle) } - return chatDao.getChatMessageForConversation(internalConversationId, messageId) - .map(ChatMessageEntity::asModel) + return chatDao.getChatMessageForConversation( + internalConversationId, + messageId + ).map(ChatMessageEntity::asModel) } @Suppress("UNCHECKED_CAST", "MagicNumber", "Detekt.TooGenericExceptionCaught") @@ -660,11 +659,12 @@ class OfflineFirstChatRepository @Inject constructor( internalConversationId = internalConversationId, accountId = conversationModel.accountId, token = conversationModel.token, + threadId = threadId, oldestMessageId = oldestMessageIdForNewChatBlock, newestMessageId = newestMessageIdForNewChatBlock, hasHistory = hasHistory ) - chatBlocksDao.upsertChatBlock(newChatBlock) + chatBlocksDao.upsertChatBlock(newChatBlock) // crash when no conversation thread exists! updateBlocks(newChatBlock) return chatMessagesFromSyncToProcess @@ -721,7 +721,11 @@ class OfflineFirstChatRepository @Inject constructor( var blockContainingQueriedMessage: ChatBlockEntity? = null if (queriedMessageId != null) { val blocksContainingQueriedMessage = - chatBlocksDao.getChatBlocksContainingMessageId(internalConversationId, queriedMessageId.toLong()) + chatBlocksDao.getChatBlocksContainingMessageId( + internalConversationId = internalConversationId, + threadId = threadId, + messageId = queriedMessageId.toLong() + ) val chatBlocks = blocksContainingQueriedMessage.first() if (chatBlocks.size > 1) { @@ -740,9 +744,10 @@ class OfflineFirstChatRepository @Inject constructor( private suspend fun updateBlocks(chatBlock: ChatBlockEntity): ChatBlockEntity? { val connectedChatBlocks = chatBlocksDao.getConnectedChatBlocks( - internalConversationId, - chatBlock.oldestMessageId, - chatBlock.newestMessageId + internalConversationId = internalConversationId, + threadId = threadId, + oldestMessageId = chatBlock.oldestMessageId, + newestMessageId = chatBlock.newestMessageId ).first() return if (connectedChatBlocks.size == 1) { @@ -769,7 +774,7 @@ class OfflineFirstChatRepository @Inject constructor( internalConversationId = internalConversationId, accountId = conversationModel.accountId, token = conversationModel.token, - threadId = conversationModel.threadId, + threadId = threadId, oldestMessageId = oldestIdFromDbChatBlocks, newestMessageId = newestIdFromDbChatBlocks, hasHistory = hasHistory @@ -793,7 +798,8 @@ class OfflineFirstChatRepository @Inject constructor( chatDao.getMessagesForConversationBeforeAndEqual( internalConversationId, messageId, - messageLimit + messageLimit, + threadId ).map { it.map(ChatMessageEntity::asModel) }.first() @@ -807,7 +813,8 @@ class OfflineFirstChatRepository @Inject constructor( chatDao.getMessagesForConversationBefore( internalConversationId, messageId, - messageLimit + messageLimit, + threadId ).map { it.map(ChatMessageEntity::asModel) }.first() @@ -870,7 +877,8 @@ class OfflineFirstChatRepository @Inject constructor( val sentMessage = chatDao.getTempMessageForConversation( internalConversationId, - referenceId + referenceId, + threadId ).firstOrNull() sentMessage?.let { @@ -886,7 +894,8 @@ class OfflineFirstChatRepository @Inject constructor( val failedMessage = chatDao.getTempMessageForConversation( internalConversationId, - referenceId + referenceId, + threadId ).firstOrNull() failedMessage?.let { it.sendStatus = SendStatus.FAILED @@ -909,7 +918,11 @@ class OfflineFirstChatRepository @Inject constructor( sendWithoutNotification: Boolean, referenceId: String ): Flow> { - val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).firstOrNull() + val messageToResend = chatDao.getTempMessageForConversation( + internalConversationId, + referenceId, + threadId + ).firstOrNull() return if (messageToResend != null) { messageToResend.sendStatus = SendStatus.PENDING chatDao.updateChatMessage(messageToResend) @@ -958,8 +971,7 @@ class OfflineFirstChatRepository @Inject constructor( try { val messageToEdit = chatDao.getChatMessageForConversation( internalConversationId, - message.jsonMessageId - .toLong() + message.jsonMessageId.toLong() ).first() messageToEdit.message = editedMessageText chatDao.upsertChatMessage(messageToEdit) @@ -973,7 +985,7 @@ class OfflineFirstChatRepository @Inject constructor( } override suspend fun sendUnsentChatMessages(credentials: String, url: String) { - val tempMessages = chatDao.getTempUnsentMessagesForConversation(internalConversationId).first() + val tempMessages = chatDao.getTempUnsentMessagesForConversation(internalConversationId, threadId).first() tempMessages.sortedBy { it.internalId }.onEach { sendChatMessage( credentials, diff --git a/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatBlocksDao.kt b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatBlocksDao.kt index af93974d6..40ac38a28 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatBlocksDao.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatBlocksDao.kt @@ -20,15 +20,17 @@ interface ChatBlocksDao { @Delete fun deleteChatBlocks(blocks: List) - @Query( - """ - SELECT * - FROM ChatBlocks - WHERE internalConversationId in (:internalConversationId) - ORDER BY newestMessageId ASC - """ - ) - fun getChatBlocks(internalConversationId: String): Flow> + // @Query( + // """ + // SELECT * + // FROM ChatBlocks + // WHERE internalConversationId in (:internalConversationId) + // ORDER BY newestMessageId ASC + // """ + // ) + // fun getChatBlocks( + // internalConversationId: String + // ): Flow> // @Query( // """ @@ -50,18 +52,24 @@ interface ChatBlocksDao { SELECT * FROM ChatBlocks WHERE internalConversationId in (:internalConversationId) + AND (:threadId IS NULL OR threadId = :threadId) AND oldestMessageId <= :messageId AND newestMessageId >= :messageId ORDER BY newestMessageId ASC """ ) - fun getChatBlocksContainingMessageId(internalConversationId: String, messageId: Long): Flow> + fun getChatBlocksContainingMessageId( + internalConversationId: String, + threadId: Long?, + messageId: Long + ): Flow> @Query( """ SELECT * FROM ChatBlocks WHERE internalConversationId = :internalConversationId + AND (:threadId IS NULL OR threadId = :threadId) AND( (oldestMessageId <= :oldestMessageId AND newestMessageId >= :oldestMessageId) OR @@ -74,20 +82,23 @@ interface ChatBlocksDao { ) fun getConnectedChatBlocks( internalConversationId: String, + threadId: Long?, oldestMessageId: Long, newestMessageId: Long ): Flow> - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsertChatBlock(chatBlock: ChatBlockEntity) - @Query( """ - DELETE FROM ChatBlocks - WHERE internalConversationId LIKE :pattern + SELECT MAX(newestMessageId) as max_items + FROM ChatBlocks + WHERE internalConversationId = :internalConversationId + AND (:threadId IS NULL OR threadId = :threadId) """ ) - fun clearChatBlocksForUser(pattern: String) + fun getNewestMessageIdFromChatBlocks(internalConversationId: String, threadId: Long?): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertChatBlock(chatBlock: ChatBlockEntity) @Query( """ diff --git a/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt index 70a17a0ef..961659472 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt @@ -18,15 +18,19 @@ import kotlinx.coroutines.flow.Flow @Dao @Suppress("Detekt.TooManyFunctions") interface ChatMessagesDao { - @Query( - """ - SELECT MAX(id) as max_items - FROM ChatMessages - WHERE internalConversationId = :internalConversationId - AND isTemporary = 0 - """ - ) - fun getNewestMessageId(internalConversationId: String): Long + // @Query( + // """ + // SELECT MAX(id) as max_items + // FROM ChatMessages + // WHERE internalConversationId = :internalConversationId + // AND isTemporary = 0 + // AND (:threadId IS NULL OR threadId = :threadId) + // """ + // ) + // fun getNewestMessageId( + // internalConversationId: String, + // threadId: Long? + // ): Long @Query( """ @@ -45,10 +49,11 @@ interface ChatMessagesDao { FROM ChatMessages WHERE internalConversationId = :internalConversationId AND isTemporary = 1 + AND (:threadId IS NULL OR threadId = :threadId) ORDER BY timestamp DESC, id DESC """ ) - fun getTempMessagesForConversation(internalConversationId: String): Flow> + fun getTempMessagesForConversation(internalConversationId: String, threadId: Long?): Flow> @Query( """ @@ -57,10 +62,14 @@ interface ChatMessagesDao { WHERE internalConversationId = :internalConversationId AND isTemporary = 1 AND sendStatus != 'SENT_PENDING_ACK' + AND (:threadId IS NULL OR threadId = :threadId) ORDER BY timestamp DESC, id DESC """ ) - fun getTempUnsentMessagesForConversation(internalConversationId: String): Flow> + fun getTempUnsentMessagesForConversation( + internalConversationId: String, + threadId: Long? + ): Flow> @Query( """ @@ -69,10 +78,15 @@ interface ChatMessagesDao { WHERE internalConversationId = :internalConversationId AND referenceId = :referenceId AND isTemporary = 1 + AND (:threadId IS NULL OR threadId = :threadId) ORDER BY timestamp DESC, id DESC """ ) - fun getTempMessageForConversation(internalConversationId: String, referenceId: String): Flow + fun getTempMessageForConversation( + internalConversationId: String, + referenceId: String, + threadId: Long? + ): Flow @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsertChatMessages(chatMessages: List) @@ -84,7 +98,8 @@ interface ChatMessagesDao { """ SELECT * FROM ChatMessages - WHERE internalConversationId = :internalConversationId AND id = :messageId + WHERE internalConversationId = :internalConversationId + AND id = :messageId """ ) fun getChatMessageForConversation(internalConversationId: String, messageId: Long): Flow @@ -126,10 +141,15 @@ interface ChatMessagesDao { FROM ChatMessages WHERE internalConversationId = :internalConversationId AND id >= :messageId AND isTemporary = 0 + AND (:threadId IS NULL OR threadId = :threadId) ORDER BY timestamp ASC, id ASC """ ) - fun getMessagesForConversationSince(internalConversationId: String, messageId: Long): Flow> + fun getMessagesForConversationSince( + internalConversationId: String, + messageId: Long, + threadId: Long? + ): Flow> @Query( """ @@ -138,6 +158,7 @@ interface ChatMessagesDao { WHERE internalConversationId = :internalConversationId AND isTemporary = 0 AND id < :messageId + AND (:threadId IS NULL OR threadId = :threadId) ORDER BY timestamp DESC, id DESC LIMIT :limit """ @@ -145,7 +166,8 @@ interface ChatMessagesDao { fun getMessagesForConversationBefore( internalConversationId: String, messageId: Long, - limit: Int + limit: Int, + threadId: Long? ): Flow> @Query( @@ -155,6 +177,7 @@ interface ChatMessagesDao { WHERE internalConversationId = :internalConversationId AND isTemporary = 0 AND id <= :messageId + AND (:threadId IS NULL OR threadId = :threadId) ORDER BY timestamp DESC, id DESC LIMIT :limit """ @@ -162,7 +185,8 @@ interface ChatMessagesDao { fun getMessagesForConversationBeforeAndEqual( internalConversationId: String, messageId: Long, - limit: Int + limit: Int, + threadId: Long? ): Flow> @Query( @@ -171,10 +195,16 @@ interface ChatMessagesDao { FROM ChatMessages WHERE internalConversationId = :internalConversationId AND isTemporary = 0 + AND (:threadId IS NULL OR threadId = :threadId) AND id BETWEEN :newestMessageId AND :oldestMessageId """ ) - fun getCountBetweenMessageIds(internalConversationId: String, oldestMessageId: Long, newestMessageId: Long): Int + fun getCountBetweenMessageIds( + internalConversationId: String, + oldestMessageId: Long, + newestMessageId: Long, + threadId: Long? + ): Int @Query( """ diff --git a/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt index 0f0456788..e6fa15a1e 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt @@ -19,8 +19,9 @@ fun ChatMessageJson.asEntity(accountId: Long) = accountId = accountId, id = id, internalConversationId = "$accountId@$token", - topmostParentId = topmostParentId, - childrenCount = childrenCount, + threadId = threadId, + isThread = hasThread, + // childrenCount = childrenCount, message = message!!, token = token!!, actorType = actorType!!, @@ -50,8 +51,9 @@ fun ChatMessageEntity.asModel() = jsonMessageId = id.toInt(), message = message, token = token, - topmostParentId = topmostParentId, - childrenCount = childrenCount, + threadId = threadId, + isThread = isThread, + // childrenCount = childrenCount, actorType = actorType, actorId = actorId, actorDisplayName = actorDisplayName, @@ -82,8 +84,9 @@ fun ChatMessageJson.asModel() = jsonMessageId = id.toInt(), message = message, token = token, - topmostParentId = topmostParentId, - childrenCount = childrenCount, + threadId = threadId, + isThread = hasThread, + // childrenCount = childrenCount, actorType = actorType, actorId = actorId, actorDisplayName = actorDisplayName, diff --git a/app/src/main/java/com/nextcloud/talk/data/database/model/ChatBlockEntity.kt b/app/src/main/java/com/nextcloud/talk/data/database/model/ChatBlockEntity.kt index da9ba8bb0..62ec1b4de 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/model/ChatBlockEntity.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/model/ChatBlockEntity.kt @@ -31,7 +31,7 @@ import androidx.room.PrimaryKey data class ChatBlockEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var id: Long = 0, - // accountId@token(@threadId) + // accountId@token @ColumnInfo(name = "internalConversationId") var internalConversationId: String, @ColumnInfo(name = "accountId") var accountId: Long? = null, @ColumnInfo(name = "token") var token: String?, diff --git a/app/src/main/java/com/nextcloud/talk/data/database/model/ChatMessageEntity.kt b/app/src/main/java/com/nextcloud/talk/data/database/model/ChatMessageEntity.kt index ac2ac36fb..775b324ea 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/model/ChatMessageEntity.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/model/ChatMessageEntity.kt @@ -41,7 +41,8 @@ data class ChatMessageEntity( @ColumnInfo(name = "id") var id: Long = 0, // accountId@roomtoken @ColumnInfo(name = "internalConversationId") var internalConversationId: String, - @ColumnInfo(name = "topmostParentId") var topmostParentId: Long? = null, + @ColumnInfo(name = "threadId") var threadId: Long? = null, + @ColumnInfo(name = "isThread") var isThread: Boolean = false, @ColumnInfo(name = "actorDisplayName") var actorDisplayName: String, @ColumnInfo(name = "message") var message: String, diff --git a/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt b/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt index 1ec105ebb..8cdd4db58 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt @@ -49,8 +49,6 @@ data class ConversationEntity( // exactly what we want for this case. @ColumnInfo(name = "token") var token: String, - @ColumnInfo(name = "threadId") var threadId: Long? = null, - @ColumnInfo(name = "displayName") var displayName: String, // OTHER ATTRIBUTES IN ALPHABETICAL ORDER diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt index 21d7b0b42..7d68c61f1 100644 --- a/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt @@ -350,18 +350,18 @@ object Migrations { db.execSQL( "ALTER TABLE ChatMessages " + - "ADD COLUMN topmostParentId INTEGER DEFAULT NULL;" + "ADD COLUMN threadId INTEGER DEFAULT NULL;" ) db.execSQL( "ALTER TABLE ChatMessages " + - "ADD COLUMN childrenCount INTEGER DEFAULT 0;" + "ADD COLUMN isThread BOOLEAN DEFAULT 0;" ) - db.execSQL( - "ALTER TABLE Conversations " + - "ADD COLUMN threadId INTEGER DEFAULT NULL;" - ) + // db.execSQL( + // "ALTER TABLE ChatMessages " + + // "ADD COLUMN childrenCount INTEGER DEFAULT 0;" + // ) // Foreign key constraints are not active during migration. // At least db.execSQL("PRAGMA foreign_keys=ON;") etc did not help. diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt index ed494f44e..cbd005667 100644 --- a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt +++ b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt @@ -17,7 +17,6 @@ class ConversationModel( var internalId: String, var accountId: Long, var token: String, - var threadId: Long? = null, var name: String, var displayName: String, var description: String, diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt index 0421eac35..fa05d2d79 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt @@ -19,8 +19,13 @@ import kotlinx.parcelize.Parcelize data class ChatMessageJson( @JsonField(name = ["id"]) var id: Long = 0, @JsonField(name = ["token"]) var token: String? = null, - @JsonField(name = ["topmostParentId"]) var topmostParentId: Long? = null, - @JsonField(name = ["childrenCount"]) var childrenCount: Long? = 0, + @JsonField(name = ["threadId"]) var threadId: Long? = null, + + // Be aware that variables with "is" at the beginning will lead to the error: + // "@JsonField annotation can only be used on private fields if both getter and setter are present." + // Instead, name it with "has" at the beginning: isThread -> hasThread + @JsonField(name = ["isThread"]) var hasThread: Boolean = false, + // @JsonField(name = ["childrenCount"]) var childrenCount: Long? = 0, @JsonField(name = ["actorType"]) var actorType: String? = null, @JsonField(name = ["actorId"]) var actorId: String? = null, @JsonField(name = ["actorDisplayName"]) var actorDisplayName: String? = null, diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt index 7c568b80e..c6750a2ec 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt @@ -28,9 +28,6 @@ data class Conversation( @JsonField(name = ["token"]) var token: String = "", - @JsonField(name = ["threadId"]) - var threadId: Long? = null, - @JsonField(name = ["name"]) var name: String = "", diff --git a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt index b84d36a4a..2da7e5149 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt @@ -101,7 +101,10 @@ class ReactionsRepositoryImpl @Inject constructor( val internalConversationId = "$accountId@$roomToken" val emoji = model.emoji - val message = dao.getChatMessageForConversation(internalConversationId, id).first() + val message = dao.getChatMessageForConversation( + internalConversationId, + id + ).first() // 2. Check state of entity, create params as needed if (message.reactions == null) { diff --git a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt index 5eaac9ef9..5b7d7af09 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt @@ -23,21 +23,30 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf class DummyChatMessagesDaoImpl : ChatMessagesDao { - override fun getNewestMessageId(internalConversationId: String): Long = 0L + // override fun getNewestMessageId( + // internalConversationId: String, + // threadId: Long? + // ): Long = 0L override fun getMessagesForConversation(internalConversationId: String): Flow> = flowOf() - override fun getTempMessagesForConversation(internalConversationId: String): Flow> = - flowOf() + override fun getTempMessagesForConversation( + internalConversationId: String, + threadId: Long? + ): Flow> = flowOf() - override fun getTempUnsentMessagesForConversation(internalConversationId: String): Flow> { + override fun getTempUnsentMessagesForConversation( + internalConversationId: String, + threadId: Long? + ): Flow> { // nothing to return here as long this class is only used for the Search window return flowOf() } override fun getTempMessageForConversation( internalConversationId: String, - referenceId: String + referenceId: String, + threadId: Long? ): Flow = flowOf() override suspend fun upsertChatMessages(chatMessages: List) { /* */ } @@ -59,25 +68,29 @@ class DummyChatMessagesDaoImpl : ChatMessagesDao { override fun getMessagesForConversationSince( internalConversationId: String, - messageId: Long + messageId: Long, + threadId: Long? ): Flow> = flowOf() override fun getMessagesForConversationBefore( internalConversationId: String, messageId: Long, - limit: Int + limit: Int, + threadId: Long? ): Flow> = flowOf() override fun getMessagesForConversationBeforeAndEqual( internalConversationId: String, messageId: Long, - limit: Int + limit: Int, + threadId: Long? ): Flow> = flowOf() override fun getCountBetweenMessageIds( internalConversationId: String, oldestMessageId: Long, - newestMessageId: Long + newestMessageId: Long, + threadId: Long? ): Int = 0 override fun clearAllMessagesForUser(pattern: String) { /* */ } @@ -192,22 +205,28 @@ class DummyConversationDaoImpl : ConversationsDao { class DummyChatBlocksDaoImpl : ChatBlocksDao { override fun deleteChatBlocks(blocks: List) { /* */ } - override fun getChatBlocks(internalConversationId: String): Flow> = flowOf() + // override fun getChatBlocks( + // internalConversationId: String + // ): Flow> = flowOf() override fun getChatBlocksContainingMessageId( internalConversationId: String, + threadId: Long?, messageId: Long ): Flow> = flowOf() override fun getConnectedChatBlocks( internalConversationId: String, + threadId: Long?, oldestMessageId: Long, newestMessageId: Long ): Flow> = flowOf() + override fun getNewestMessageIdFromChatBlocks(internalConversationId: String, threadId: Long?): Long = 0L + override suspend fun upsertChatBlock(chatBlock: ChatBlockEntity) { /* */ } - override fun clearChatBlocksForUser(pattern: String) { /* */ } + // override fun clearChatBlocksForUser(pattern: String) { /* */ } override fun deleteChatBlocksOlderThan(internalConversationId: String, messageId: Long) { /* */ } }