add chatBlock handling for threads

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2025-07-03 15:26:02 +02:00
parent 7d6cdb9e0d
commit b04a9c49cf
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
19 changed files with 229 additions and 121 deletions

View File

@ -52,7 +52,8 @@ class ChatBlocksDaoTest {
@Test @Test
fun testGetConnectedChatBlocks() = fun testGetConnectedChatBlocks() =
runTest { runTest {
usersDao.saveUser(createUserEntity("account1", "Account 1")) val user = createUserEntity("account1", "Account 1")
usersDao.saveUser(user)
val account1 = usersDao.getUserWithUserId("account1").blockingGet() val account1 = usersDao.getUserWithUserId("account1").blockingGet()
conversationsDao.upsertConversations( conversationsDao.upsertConversations(
@ -77,6 +78,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId, internalConversationId = conversation1.internalId,
accountId = conversation1.accountId, accountId = conversation1.accountId,
token = conversation1.token, token = conversation1.token,
threadId = null,
oldestMessageId = 50, oldestMessageId = 50,
newestMessageId = 60, newestMessageId = 60,
hasHistory = true hasHistory = true
@ -86,6 +88,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId, internalConversationId = conversation1.internalId,
accountId = conversation1.accountId, accountId = conversation1.accountId,
token = conversation1.token, token = conversation1.token,
threadId = null,
oldestMessageId = 10, oldestMessageId = 10,
newestMessageId = 20, newestMessageId = 20,
hasHistory = true hasHistory = true
@ -95,6 +98,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId, internalConversationId = conversation1.internalId,
accountId = conversation1.accountId, accountId = conversation1.accountId,
token = conversation1.token, token = conversation1.token,
threadId = null,
oldestMessageId = 45, oldestMessageId = 45,
newestMessageId = 55, newestMessageId = 55,
hasHistory = true hasHistory = true
@ -104,6 +108,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId, internalConversationId = conversation1.internalId,
accountId = conversation1.accountId, accountId = conversation1.accountId,
token = conversation1.token, token = conversation1.token,
threadId = null,
oldestMessageId = 52, oldestMessageId = 52,
newestMessageId = 58, newestMessageId = 58,
hasHistory = true hasHistory = true
@ -113,6 +118,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId, internalConversationId = conversation1.internalId,
accountId = conversation1.accountId, accountId = conversation1.accountId,
token = conversation1.token, token = conversation1.token,
threadId = null,
oldestMessageId = 1, oldestMessageId = 1,
newestMessageId = 99, newestMessageId = 99,
hasHistory = true hasHistory = true
@ -122,6 +128,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId, internalConversationId = conversation1.internalId,
accountId = conversation1.accountId, accountId = conversation1.accountId,
token = conversation1.token, token = conversation1.token,
threadId = null,
oldestMessageId = 59, oldestMessageId = 59,
newestMessageId = 70, newestMessageId = 70,
hasHistory = true hasHistory = true
@ -131,6 +138,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation1.internalId, internalConversationId = conversation1.internalId,
accountId = conversation1.accountId, accountId = conversation1.accountId,
token = conversation1.token, token = conversation1.token,
threadId = null,
oldestMessageId = 80, oldestMessageId = 80,
newestMessageId = 90, newestMessageId = 90,
hasHistory = true hasHistory = true
@ -140,6 +148,7 @@ class ChatBlocksDaoTest {
internalConversationId = conversation2.internalId, internalConversationId = conversation2.internalId,
accountId = conversation2.accountId, accountId = conversation2.accountId,
token = conversation2.token, token = conversation2.token,
threadId = null,
oldestMessageId = 53, oldestMessageId = 53,
newestMessageId = 57, newestMessageId = 57,
hasHistory = true hasHistory = true
@ -156,9 +165,10 @@ class ChatBlocksDaoTest {
chatBlocksDao.upsertChatBlock(chatBlockWithinButOtherConversation) chatBlocksDao.upsertChatBlock(chatBlockWithinButOtherConversation)
val results = chatBlocksDao.getConnectedChatBlocks( val results = chatBlocksDao.getConnectedChatBlocks(
conversation1.internalId, internalConversationId = conversation1.internalId,
searchedChatBlock.oldestMessageId, threadId = null,
searchedChatBlock.newestMessageId oldestMessageId = searchedChatBlock.oldestMessageId,
newestMessageId = searchedChatBlock.newestMessageId
) )
assertEquals(5, results.first().size) assertEquals(5, results.first().size)

View File

@ -140,7 +140,11 @@ class ChatMessagesDaoTest {
assertEquals("are", conv1chatMessage3.message) assertEquals("are", conv1chatMessage3.message)
val chatMessagesConv1Since = val chatMessagesConv1Since =
chatMessagesDao.getMessagesForConversationSince(conversation1.internalId, conv1chatMessage3.id) chatMessagesDao.getMessagesForConversationSince(
conversation1.internalId,
conv1chatMessage3.id,
null
)
assertEquals(3, chatMessagesConv1Since.first().size) assertEquals(3, chatMessagesConv1Since.first().size)
assertEquals("are", chatMessagesConv1Since.first()[0].message) assertEquals("are", chatMessagesConv1Since.first()[0].message)
assertEquals("some", chatMessagesConv1Since.first()[1].message) assertEquals("some", chatMessagesConv1Since.first()[1].message)
@ -150,7 +154,8 @@ class ChatMessagesDaoTest {
chatMessagesDao.getMessagesForConversationBeforeAndEqual( chatMessagesDao.getMessagesForConversationBeforeAndEqual(
conversation1.internalId, conversation1.internalId,
conv1chatMessage3.id, conv1chatMessage3.id,
3 3,
null
) )
assertEquals(3, chatMessagesConv1To.first().size) assertEquals(3, chatMessagesConv1To.first().size)
assertEquals("hello", chatMessagesConv1To.first()[2].message) assertEquals("hello", chatMessagesConv1To.first()[2].message)

View File

@ -10,6 +10,7 @@
package com.nextcloud.talk.adapters.messages package com.nextcloud.talk.adapters.messages
import android.content.Context import android.content.Context
import android.text.SpannableStringBuilder
import android.util.Log import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
@ -145,7 +146,10 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
binding.messageAuthor.visibility = View.GONE binding.messageAuthor.visibility = View.GONE
} }
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) 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 { } else {
binding.messageText.visibility = View.GONE binding.messageText.visibility = View.GONE
binding.checkboxContainer.visibility = View.VISIBLE binding.checkboxContainer.visibility = View.VISIBLE

View File

@ -10,6 +10,7 @@
package com.nextcloud.talk.adapters.messages package com.nextcloud.talk.adapters.messages
import android.content.Context import android.content.Context
import android.text.SpannableStringBuilder
import android.util.Log import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
@ -159,7 +160,10 @@ class OutcomingTextMessageViewHolder(itemView: View) :
binding.messageTime.layoutParams = layoutParams binding.messageTime.layoutParams = layoutParams
viewThemeUtils.platform.colorTextView(binding.messageText, ColorRole.ON_SURFACE_VARIANT) 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 { } else {
binding.messageText.visibility = View.GONE binding.messageText.visibility = View.GONE
binding.checkboxContainer.visibility = View.VISIBLE binding.checkboxContainer.visibility = View.VISIBLE

View File

@ -557,7 +557,12 @@ class ChatActivity :
val extras: Bundle? = intent.extras val extras: Bundle? = intent.extras
roomToken = extras?.getString(KEY_ROOM_TOKEN).orEmpty() 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() sharedText = extras?.getString(BundleKeys.KEY_SHARED_TEXT).orEmpty()

View File

@ -45,9 +45,11 @@ data class ChatMessage(
var token: String? = null, 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 // guests or users
var actorType: String? = null, var actorType: String? = null,

View File

@ -122,16 +122,10 @@ class OfflineFirstChatRepository @Inject constructor(
private var threadId: Long? = null private var threadId: Long? = null
override fun initData(credentials: String, urlForChatting: String, roomToken: String, threadId: Long?) { override fun initData(credentials: String, urlForChatting: String, roomToken: String, threadId: Long?) {
val threadIdAppendedString = if (threadId != null && threadId > 0) { internalConversationId = currentUser.id.toString() + "@" + roomToken
"@$threadId"
} else {
""
}
internalConversationId = currentUser.id.toString() + "@" + roomToken + threadIdAppendedString
this.credentials = credentials this.credentials = credentials
this.urlForChatting = urlForChatting this.urlForChatting = urlForChatting
this.threadId = threadId // use this threadId in API requests when fetching messages? + this.threadId = threadId
// Introduce ChatBlocks for threads
} }
override fun updateConversation(conversationModel: ConversationModel) { override fun updateConversation(conversationModel: ConversationModel) {
@ -151,7 +145,8 @@ class OfflineFirstChatRepository @Inject constructor(
Log.d(TAG, "conversationModel.internalId: " + conversationModel.internalId) Log.d(TAG, "conversationModel.internalId: " + conversationModel.internalId)
Log.d(TAG, "conversationModel.lastReadMessage:" + conversationModel.lastReadMessage) 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") Log.d(TAG, "newestMessageIdFromDb: $newestMessageIdFromDb")
val weAlreadyHaveSomeOfflineMessages = newestMessageIdFromDb > 0 val weAlreadyHaveSomeOfflineMessages = newestMessageIdFromDb > 0
@ -197,7 +192,7 @@ class OfflineFirstChatRepository @Inject constructor(
Log.e(TAG, "initial loading of messages failed") Log.e(TAG, "initial loading of messages failed")
} }
newestMessageIdFromDb = chatDao.getNewestMessageId(internalConversationId) newestMessageIdFromDb = chatBlocksDao.getNewestMessageIdFromChatBlocks(internalConversationId, threadId)
Log.d(TAG, "newestMessageIdFromDb after sync: $newestMessageIdFromDb") Log.d(TAG, "newestMessageIdFromDb after sync: $newestMessageIdFromDb")
} }
@ -211,9 +206,9 @@ class OfflineFirstChatRepository @Inject constructor(
val limit = getCappedMessagesAmountOfChatBlock(newestMessageIdFromDb) val limit = getCappedMessagesAmountOfChatBlock(newestMessageIdFromDb)
val list = getMessagesBeforeAndEqual( val list = getMessagesBeforeAndEqual(
newestMessageIdFromDb, messageId = newestMessageIdFromDb,
internalConversationId, internalConversationId = internalConversationId,
limit messageLimit = limit
) )
if (list.isNotEmpty()) { if (list.isNotEmpty()) {
handleNewAndTempMessages( handleNewAndTempMessages(
@ -242,7 +237,8 @@ class OfflineFirstChatRepository @Inject constructor(
val amountBetween = chatDao.getCountBetweenMessageIds( val amountBetween = chatDao.getCountBetweenMessageIds(
internalConversationId, internalConversationId,
messageId, messageId,
chatBlock.oldestMessageId chatBlock.oldestMessageId,
threadId
) )
Log.d(TAG, "amount of messages between newestMessageId and oldest message of same ChatBlock:$amountBetween") 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) withNetworkParams.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap)
val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId) val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId, DEFAULT_MESSAGES_LIMIT)
if (loadFromServer) { if (loadFromServer) {
Log.d(TAG, "Starting online request for loadMoreMessages") Log.d(TAG, "Starting online request for loadMoreMessages")
@ -354,7 +350,10 @@ class OfflineFirstChatRepository @Inject constructor(
updateUiForLastCommonRead() updateUiForLastCommonRead()
val newestMessage = chatDao.getNewestMessageId(internalConversationId).toInt() val newestMessage = chatBlocksDao.getNewestMessageIdFromChatBlocks(
internalConversationId,
threadId
).toInt()
// update field map vars for next cycle // update field map vars for next cycle
fieldMap = getFieldMap( fieldMap = getFieldMap(
@ -380,7 +379,7 @@ class OfflineFirstChatRepository @Inject constructor(
} }
// remove all temp messages from UI // remove all temp messages from UI
val oldTempMessages = chatDao.getTempMessagesForConversation(internalConversationId) val oldTempMessages = chatDao.getTempMessagesForConversation(internalConversationId, threadId)
.first() .first()
.map(ChatMessageEntity::asModel) .map(ChatMessageEntity::asModel)
oldTempMessages.forEach { oldTempMessages.forEach {
@ -404,7 +403,7 @@ class OfflineFirstChatRepository @Inject constructor(
) )
// add the remaining temp messages to UI again // add the remaining temp messages to UI again
val remainingTempMessages = chatDao.getTempMessagesForConversation(internalConversationId) val remainingTempMessages = chatDao.getTempMessagesForConversation(internalConversationId, threadId)
.first() .first()
.sortedBy { it.internalId } .sortedBy { it.internalId }
.map(ChatMessageEntity::asModel) .map(ChatMessageEntity::asModel)
@ -417,7 +416,7 @@ class OfflineFirstChatRepository @Inject constructor(
_messageFlow.emit(triple) _messageFlow.emit(triple)
} }
private suspend fun hasToLoadPreviousMessagesFromServer(beforeMessageId: Long): Boolean { private suspend fun hasToLoadPreviousMessagesFromServer(beforeMessageId: Long, amountToCheck: Int): Boolean {
val loadFromServer: Boolean val loadFromServer: Boolean
val blockForMessage = getBlockOfMessage(beforeMessageId.toInt()) 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") Log.d(TAG, "The last chatBlock is reached so we won't request server for older messages")
loadFromServer = false loadFromServer = false
} else { } 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( val amountBetween = chatDao.getCountBetweenMessageIds(
internalConversationId, internalConversationId,
beforeMessageId, beforeMessageId,
blockForMessage.oldestMessageId blockForMessage.oldestMessageId,
threadId
) )
loadFromServer = amountBetween < DEFAULT_MESSAGES_LIMIT loadFromServer = amountBetween < amountToCheck
Log.d( Log.d(
TAG, TAG,
"Amount between messageId " + beforeMessageId + " and " + blockForMessage.oldestMessageId + "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 return loadFromServer
@ -479,7 +476,7 @@ class OfflineFirstChatRepository @Inject constructor(
override suspend fun getMessage(messageId: Long, bundle: Bundle): Flow<ChatMessage> { override suspend fun getMessage(messageId: Long, bundle: Bundle): Flow<ChatMessage> {
Log.d(TAG, "Get message with id $messageId") Log.d(TAG, "Get message with id $messageId")
val loadFromServer = hasToLoadPreviousMessagesFromServer(messageId) val loadFromServer = hasToLoadPreviousMessagesFromServer(messageId, 1)
if (loadFromServer) { if (loadFromServer) {
val fieldMap = getFieldMap( val fieldMap = getFieldMap(
@ -495,8 +492,10 @@ class OfflineFirstChatRepository @Inject constructor(
Log.d(TAG, "Starting online request for single message (e.g. a reply)") Log.d(TAG, "Starting online request for single message (e.g. a reply)")
sync(bundle) sync(bundle)
} }
return chatDao.getChatMessageForConversation(internalConversationId, messageId) return chatDao.getChatMessageForConversation(
.map(ChatMessageEntity::asModel) internalConversationId,
messageId
).map(ChatMessageEntity::asModel)
} }
@Suppress("UNCHECKED_CAST", "MagicNumber", "Detekt.TooGenericExceptionCaught") @Suppress("UNCHECKED_CAST", "MagicNumber", "Detekt.TooGenericExceptionCaught")
@ -660,11 +659,12 @@ class OfflineFirstChatRepository @Inject constructor(
internalConversationId = internalConversationId, internalConversationId = internalConversationId,
accountId = conversationModel.accountId, accountId = conversationModel.accountId,
token = conversationModel.token, token = conversationModel.token,
threadId = threadId,
oldestMessageId = oldestMessageIdForNewChatBlock, oldestMessageId = oldestMessageIdForNewChatBlock,
newestMessageId = newestMessageIdForNewChatBlock, newestMessageId = newestMessageIdForNewChatBlock,
hasHistory = hasHistory hasHistory = hasHistory
) )
chatBlocksDao.upsertChatBlock(newChatBlock) chatBlocksDao.upsertChatBlock(newChatBlock) // crash when no conversation thread exists!
updateBlocks(newChatBlock) updateBlocks(newChatBlock)
return chatMessagesFromSyncToProcess return chatMessagesFromSyncToProcess
@ -721,7 +721,11 @@ class OfflineFirstChatRepository @Inject constructor(
var blockContainingQueriedMessage: ChatBlockEntity? = null var blockContainingQueriedMessage: ChatBlockEntity? = null
if (queriedMessageId != null) { if (queriedMessageId != null) {
val blocksContainingQueriedMessage = val blocksContainingQueriedMessage =
chatBlocksDao.getChatBlocksContainingMessageId(internalConversationId, queriedMessageId.toLong()) chatBlocksDao.getChatBlocksContainingMessageId(
internalConversationId = internalConversationId,
threadId = threadId,
messageId = queriedMessageId.toLong()
)
val chatBlocks = blocksContainingQueriedMessage.first() val chatBlocks = blocksContainingQueriedMessage.first()
if (chatBlocks.size > 1) { if (chatBlocks.size > 1) {
@ -740,9 +744,10 @@ class OfflineFirstChatRepository @Inject constructor(
private suspend fun updateBlocks(chatBlock: ChatBlockEntity): ChatBlockEntity? { private suspend fun updateBlocks(chatBlock: ChatBlockEntity): ChatBlockEntity? {
val connectedChatBlocks = val connectedChatBlocks =
chatBlocksDao.getConnectedChatBlocks( chatBlocksDao.getConnectedChatBlocks(
internalConversationId, internalConversationId = internalConversationId,
chatBlock.oldestMessageId, threadId = threadId,
chatBlock.newestMessageId oldestMessageId = chatBlock.oldestMessageId,
newestMessageId = chatBlock.newestMessageId
).first() ).first()
return if (connectedChatBlocks.size == 1) { return if (connectedChatBlocks.size == 1) {
@ -769,7 +774,7 @@ class OfflineFirstChatRepository @Inject constructor(
internalConversationId = internalConversationId, internalConversationId = internalConversationId,
accountId = conversationModel.accountId, accountId = conversationModel.accountId,
token = conversationModel.token, token = conversationModel.token,
threadId = conversationModel.threadId, threadId = threadId,
oldestMessageId = oldestIdFromDbChatBlocks, oldestMessageId = oldestIdFromDbChatBlocks,
newestMessageId = newestIdFromDbChatBlocks, newestMessageId = newestIdFromDbChatBlocks,
hasHistory = hasHistory hasHistory = hasHistory
@ -793,7 +798,8 @@ class OfflineFirstChatRepository @Inject constructor(
chatDao.getMessagesForConversationBeforeAndEqual( chatDao.getMessagesForConversationBeforeAndEqual(
internalConversationId, internalConversationId,
messageId, messageId,
messageLimit messageLimit,
threadId
).map { ).map {
it.map(ChatMessageEntity::asModel) it.map(ChatMessageEntity::asModel)
}.first() }.first()
@ -807,7 +813,8 @@ class OfflineFirstChatRepository @Inject constructor(
chatDao.getMessagesForConversationBefore( chatDao.getMessagesForConversationBefore(
internalConversationId, internalConversationId,
messageId, messageId,
messageLimit messageLimit,
threadId
).map { ).map {
it.map(ChatMessageEntity::asModel) it.map(ChatMessageEntity::asModel)
}.first() }.first()
@ -870,7 +877,8 @@ class OfflineFirstChatRepository @Inject constructor(
val sentMessage = chatDao.getTempMessageForConversation( val sentMessage = chatDao.getTempMessageForConversation(
internalConversationId, internalConversationId,
referenceId referenceId,
threadId
).firstOrNull() ).firstOrNull()
sentMessage?.let { sentMessage?.let {
@ -886,7 +894,8 @@ class OfflineFirstChatRepository @Inject constructor(
val failedMessage = chatDao.getTempMessageForConversation( val failedMessage = chatDao.getTempMessageForConversation(
internalConversationId, internalConversationId,
referenceId referenceId,
threadId
).firstOrNull() ).firstOrNull()
failedMessage?.let { failedMessage?.let {
it.sendStatus = SendStatus.FAILED it.sendStatus = SendStatus.FAILED
@ -909,7 +918,11 @@ class OfflineFirstChatRepository @Inject constructor(
sendWithoutNotification: Boolean, sendWithoutNotification: Boolean,
referenceId: String referenceId: String
): Flow<Result<ChatMessage?>> { ): Flow<Result<ChatMessage?>> {
val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).firstOrNull() val messageToResend = chatDao.getTempMessageForConversation(
internalConversationId,
referenceId,
threadId
).firstOrNull()
return if (messageToResend != null) { return if (messageToResend != null) {
messageToResend.sendStatus = SendStatus.PENDING messageToResend.sendStatus = SendStatus.PENDING
chatDao.updateChatMessage(messageToResend) chatDao.updateChatMessage(messageToResend)
@ -958,8 +971,7 @@ class OfflineFirstChatRepository @Inject constructor(
try { try {
val messageToEdit = chatDao.getChatMessageForConversation( val messageToEdit = chatDao.getChatMessageForConversation(
internalConversationId, internalConversationId,
message.jsonMessageId message.jsonMessageId.toLong()
.toLong()
).first() ).first()
messageToEdit.message = editedMessageText messageToEdit.message = editedMessageText
chatDao.upsertChatMessage(messageToEdit) chatDao.upsertChatMessage(messageToEdit)
@ -973,7 +985,7 @@ class OfflineFirstChatRepository @Inject constructor(
} }
override suspend fun sendUnsentChatMessages(credentials: String, url: String) { 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 { tempMessages.sortedBy { it.internalId }.onEach {
sendChatMessage( sendChatMessage(
credentials, credentials,

View File

@ -20,15 +20,17 @@ interface ChatBlocksDao {
@Delete @Delete
fun deleteChatBlocks(blocks: List<ChatBlockEntity>) fun deleteChatBlocks(blocks: List<ChatBlockEntity>)
@Query( // @Query(
""" // """
SELECT * // SELECT *
FROM ChatBlocks // FROM ChatBlocks
WHERE internalConversationId in (:internalConversationId) // WHERE internalConversationId in (:internalConversationId)
ORDER BY newestMessageId ASC // ORDER BY newestMessageId ASC
""" // """
) // )
fun getChatBlocks(internalConversationId: String): Flow<List<ChatBlockEntity>> // fun getChatBlocks(
// internalConversationId: String
// ): Flow<List<ChatBlockEntity>>
// @Query( // @Query(
// """ // """
@ -50,18 +52,24 @@ interface ChatBlocksDao {
SELECT * SELECT *
FROM ChatBlocks FROM ChatBlocks
WHERE internalConversationId in (:internalConversationId) WHERE internalConversationId in (:internalConversationId)
AND (:threadId IS NULL OR threadId = :threadId)
AND oldestMessageId <= :messageId AND oldestMessageId <= :messageId
AND newestMessageId >= :messageId AND newestMessageId >= :messageId
ORDER BY newestMessageId ASC ORDER BY newestMessageId ASC
""" """
) )
fun getChatBlocksContainingMessageId(internalConversationId: String, messageId: Long): Flow<List<ChatBlockEntity?>> fun getChatBlocksContainingMessageId(
internalConversationId: String,
threadId: Long?,
messageId: Long
): Flow<List<ChatBlockEntity?>>
@Query( @Query(
""" """
SELECT * SELECT *
FROM ChatBlocks FROM ChatBlocks
WHERE internalConversationId = :internalConversationId WHERE internalConversationId = :internalConversationId
AND (:threadId IS NULL OR threadId = :threadId)
AND( AND(
(oldestMessageId <= :oldestMessageId AND newestMessageId >= :oldestMessageId) (oldestMessageId <= :oldestMessageId AND newestMessageId >= :oldestMessageId)
OR OR
@ -74,20 +82,23 @@ interface ChatBlocksDao {
) )
fun getConnectedChatBlocks( fun getConnectedChatBlocks(
internalConversationId: String, internalConversationId: String,
threadId: Long?,
oldestMessageId: Long, oldestMessageId: Long,
newestMessageId: Long newestMessageId: Long
): Flow<List<ChatBlockEntity>> ): Flow<List<ChatBlockEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertChatBlock(chatBlock: ChatBlockEntity)
@Query( @Query(
""" """
DELETE FROM ChatBlocks SELECT MAX(newestMessageId) as max_items
WHERE internalConversationId LIKE :pattern 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( @Query(
""" """

View File

@ -18,15 +18,19 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
@Suppress("Detekt.TooManyFunctions") @Suppress("Detekt.TooManyFunctions")
interface ChatMessagesDao { interface ChatMessagesDao {
@Query( // @Query(
""" // """
SELECT MAX(id) as max_items // SELECT MAX(id) as max_items
FROM ChatMessages // FROM ChatMessages
WHERE internalConversationId = :internalConversationId // WHERE internalConversationId = :internalConversationId
AND isTemporary = 0 // AND isTemporary = 0
""" // AND (:threadId IS NULL OR threadId = :threadId)
) // """
fun getNewestMessageId(internalConversationId: String): Long // )
// fun getNewestMessageId(
// internalConversationId: String,
// threadId: Long?
// ): Long
@Query( @Query(
""" """
@ -45,10 +49,11 @@ interface ChatMessagesDao {
FROM ChatMessages FROM ChatMessages
WHERE internalConversationId = :internalConversationId WHERE internalConversationId = :internalConversationId
AND isTemporary = 1 AND isTemporary = 1
AND (:threadId IS NULL OR threadId = :threadId)
ORDER BY timestamp DESC, id DESC ORDER BY timestamp DESC, id DESC
""" """
) )
fun getTempMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> fun getTempMessagesForConversation(internalConversationId: String, threadId: Long?): Flow<List<ChatMessageEntity>>
@Query( @Query(
""" """
@ -57,10 +62,14 @@ interface ChatMessagesDao {
WHERE internalConversationId = :internalConversationId WHERE internalConversationId = :internalConversationId
AND isTemporary = 1 AND isTemporary = 1
AND sendStatus != 'SENT_PENDING_ACK' AND sendStatus != 'SENT_PENDING_ACK'
AND (:threadId IS NULL OR threadId = :threadId)
ORDER BY timestamp DESC, id DESC ORDER BY timestamp DESC, id DESC
""" """
) )
fun getTempUnsentMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> fun getTempUnsentMessagesForConversation(
internalConversationId: String,
threadId: Long?
): Flow<List<ChatMessageEntity>>
@Query( @Query(
""" """
@ -69,10 +78,15 @@ interface ChatMessagesDao {
WHERE internalConversationId = :internalConversationId WHERE internalConversationId = :internalConversationId
AND referenceId = :referenceId AND referenceId = :referenceId
AND isTemporary = 1 AND isTemporary = 1
AND (:threadId IS NULL OR threadId = :threadId)
ORDER BY timestamp DESC, id DESC ORDER BY timestamp DESC, id DESC
""" """
) )
fun getTempMessageForConversation(internalConversationId: String, referenceId: String): Flow<ChatMessageEntity?> fun getTempMessageForConversation(
internalConversationId: String,
referenceId: String,
threadId: Long?
): Flow<ChatMessageEntity?>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>) suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>)
@ -84,7 +98,8 @@ interface ChatMessagesDao {
""" """
SELECT * SELECT *
FROM ChatMessages FROM ChatMessages
WHERE internalConversationId = :internalConversationId AND id = :messageId WHERE internalConversationId = :internalConversationId
AND id = :messageId
""" """
) )
fun getChatMessageForConversation(internalConversationId: String, messageId: Long): Flow<ChatMessageEntity> fun getChatMessageForConversation(internalConversationId: String, messageId: Long): Flow<ChatMessageEntity>
@ -126,10 +141,15 @@ interface ChatMessagesDao {
FROM ChatMessages FROM ChatMessages
WHERE internalConversationId = :internalConversationId AND id >= :messageId WHERE internalConversationId = :internalConversationId AND id >= :messageId
AND isTemporary = 0 AND isTemporary = 0
AND (:threadId IS NULL OR threadId = :threadId)
ORDER BY timestamp ASC, id ASC ORDER BY timestamp ASC, id ASC
""" """
) )
fun getMessagesForConversationSince(internalConversationId: String, messageId: Long): Flow<List<ChatMessageEntity>> fun getMessagesForConversationSince(
internalConversationId: String,
messageId: Long,
threadId: Long?
): Flow<List<ChatMessageEntity>>
@Query( @Query(
""" """
@ -138,6 +158,7 @@ interface ChatMessagesDao {
WHERE internalConversationId = :internalConversationId WHERE internalConversationId = :internalConversationId
AND isTemporary = 0 AND isTemporary = 0
AND id < :messageId AND id < :messageId
AND (:threadId IS NULL OR threadId = :threadId)
ORDER BY timestamp DESC, id DESC ORDER BY timestamp DESC, id DESC
LIMIT :limit LIMIT :limit
""" """
@ -145,7 +166,8 @@ interface ChatMessagesDao {
fun getMessagesForConversationBefore( fun getMessagesForConversationBefore(
internalConversationId: String, internalConversationId: String,
messageId: Long, messageId: Long,
limit: Int limit: Int,
threadId: Long?
): Flow<List<ChatMessageEntity>> ): Flow<List<ChatMessageEntity>>
@Query( @Query(
@ -155,6 +177,7 @@ interface ChatMessagesDao {
WHERE internalConversationId = :internalConversationId WHERE internalConversationId = :internalConversationId
AND isTemporary = 0 AND isTemporary = 0
AND id <= :messageId AND id <= :messageId
AND (:threadId IS NULL OR threadId = :threadId)
ORDER BY timestamp DESC, id DESC ORDER BY timestamp DESC, id DESC
LIMIT :limit LIMIT :limit
""" """
@ -162,7 +185,8 @@ interface ChatMessagesDao {
fun getMessagesForConversationBeforeAndEqual( fun getMessagesForConversationBeforeAndEqual(
internalConversationId: String, internalConversationId: String,
messageId: Long, messageId: Long,
limit: Int limit: Int,
threadId: Long?
): Flow<List<ChatMessageEntity>> ): Flow<List<ChatMessageEntity>>
@Query( @Query(
@ -171,10 +195,16 @@ interface ChatMessagesDao {
FROM ChatMessages FROM ChatMessages
WHERE internalConversationId = :internalConversationId WHERE internalConversationId = :internalConversationId
AND isTemporary = 0 AND isTemporary = 0
AND (:threadId IS NULL OR threadId = :threadId)
AND id BETWEEN :newestMessageId AND :oldestMessageId 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( @Query(
""" """

View File

@ -19,8 +19,9 @@ fun ChatMessageJson.asEntity(accountId: Long) =
accountId = accountId, accountId = accountId,
id = id, id = id,
internalConversationId = "$accountId@$token", internalConversationId = "$accountId@$token",
topmostParentId = topmostParentId, threadId = threadId,
childrenCount = childrenCount, isThread = hasThread,
// childrenCount = childrenCount,
message = message!!, message = message!!,
token = token!!, token = token!!,
actorType = actorType!!, actorType = actorType!!,
@ -50,8 +51,9 @@ fun ChatMessageEntity.asModel() =
jsonMessageId = id.toInt(), jsonMessageId = id.toInt(),
message = message, message = message,
token = token, token = token,
topmostParentId = topmostParentId, threadId = threadId,
childrenCount = childrenCount, isThread = isThread,
// childrenCount = childrenCount,
actorType = actorType, actorType = actorType,
actorId = actorId, actorId = actorId,
actorDisplayName = actorDisplayName, actorDisplayName = actorDisplayName,
@ -82,8 +84,9 @@ fun ChatMessageJson.asModel() =
jsonMessageId = id.toInt(), jsonMessageId = id.toInt(),
message = message, message = message,
token = token, token = token,
topmostParentId = topmostParentId, threadId = threadId,
childrenCount = childrenCount, isThread = hasThread,
// childrenCount = childrenCount,
actorType = actorType, actorType = actorType,
actorId = actorId, actorId = actorId,
actorDisplayName = actorDisplayName, actorDisplayName = actorDisplayName,

View File

@ -31,7 +31,7 @@ import androidx.room.PrimaryKey
data class ChatBlockEntity( data class ChatBlockEntity(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") var id: Long = 0, @ColumnInfo(name = "id") var id: Long = 0,
// accountId@token(@threadId) // accountId@token
@ColumnInfo(name = "internalConversationId") var internalConversationId: String, @ColumnInfo(name = "internalConversationId") var internalConversationId: String,
@ColumnInfo(name = "accountId") var accountId: Long? = null, @ColumnInfo(name = "accountId") var accountId: Long? = null,
@ColumnInfo(name = "token") var token: String?, @ColumnInfo(name = "token") var token: String?,

View File

@ -41,7 +41,8 @@ data class ChatMessageEntity(
@ColumnInfo(name = "id") var id: Long = 0, @ColumnInfo(name = "id") var id: Long = 0,
// accountId@roomtoken // accountId@roomtoken
@ColumnInfo(name = "internalConversationId") var internalConversationId: String, @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 = "actorDisplayName") var actorDisplayName: String,
@ColumnInfo(name = "message") var message: String, @ColumnInfo(name = "message") var message: String,

View File

@ -49,8 +49,6 @@ data class ConversationEntity(
// exactly what we want for this case. // exactly what we want for this case.
@ColumnInfo(name = "token") var token: String, @ColumnInfo(name = "token") var token: String,
@ColumnInfo(name = "threadId") var threadId: Long? = null,
@ColumnInfo(name = "displayName") var displayName: String, @ColumnInfo(name = "displayName") var displayName: String,
// OTHER ATTRIBUTES IN ALPHABETICAL ORDER // OTHER ATTRIBUTES IN ALPHABETICAL ORDER

View File

@ -350,18 +350,18 @@ object Migrations {
db.execSQL( db.execSQL(
"ALTER TABLE ChatMessages " + "ALTER TABLE ChatMessages " +
"ADD COLUMN topmostParentId INTEGER DEFAULT NULL;" "ADD COLUMN threadId INTEGER DEFAULT NULL;"
) )
db.execSQL( db.execSQL(
"ALTER TABLE ChatMessages " + "ALTER TABLE ChatMessages " +
"ADD COLUMN childrenCount INTEGER DEFAULT 0;" "ADD COLUMN isThread BOOLEAN DEFAULT 0;"
) )
db.execSQL( // db.execSQL(
"ALTER TABLE Conversations " + // "ALTER TABLE ChatMessages " +
"ADD COLUMN threadId INTEGER DEFAULT NULL;" // "ADD COLUMN childrenCount INTEGER DEFAULT 0;"
) // )
// Foreign key constraints are not active during migration. // Foreign key constraints are not active during migration.
// At least db.execSQL("PRAGMA foreign_keys=ON;") etc did not help. // At least db.execSQL("PRAGMA foreign_keys=ON;") etc did not help.

View File

@ -17,7 +17,6 @@ class ConversationModel(
var internalId: String, var internalId: String,
var accountId: Long, var accountId: Long,
var token: String, var token: String,
var threadId: Long? = null,
var name: String, var name: String,
var displayName: String, var displayName: String,
var description: String, var description: String,

View File

@ -19,8 +19,13 @@ import kotlinx.parcelize.Parcelize
data class ChatMessageJson( data class ChatMessageJson(
@JsonField(name = ["id"]) var id: Long = 0, @JsonField(name = ["id"]) var id: Long = 0,
@JsonField(name = ["token"]) var token: String? = null, @JsonField(name = ["token"]) var token: String? = null,
@JsonField(name = ["topmostParentId"]) var topmostParentId: Long? = null, @JsonField(name = ["threadId"]) var threadId: Long? = null,
@JsonField(name = ["childrenCount"]) var childrenCount: Long? = 0,
// 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 = ["actorType"]) var actorType: String? = null,
@JsonField(name = ["actorId"]) var actorId: String? = null, @JsonField(name = ["actorId"]) var actorId: String? = null,
@JsonField(name = ["actorDisplayName"]) var actorDisplayName: String? = null, @JsonField(name = ["actorDisplayName"]) var actorDisplayName: String? = null,

View File

@ -28,9 +28,6 @@ data class Conversation(
@JsonField(name = ["token"]) @JsonField(name = ["token"])
var token: String = "", var token: String = "",
@JsonField(name = ["threadId"])
var threadId: Long? = null,
@JsonField(name = ["name"]) @JsonField(name = ["name"])
var name: String = "", var name: String = "",

View File

@ -101,7 +101,10 @@ class ReactionsRepositoryImpl @Inject constructor(
val internalConversationId = "$accountId@$roomToken" val internalConversationId = "$accountId@$roomToken"
val emoji = model.emoji 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 // 2. Check state of entity, create params as needed
if (message.reactions == null) { if (message.reactions == null) {

View File

@ -23,21 +23,30 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
class DummyChatMessagesDaoImpl : ChatMessagesDao { 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<List<ChatMessageEntity>> = flowOf() override fun getMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> = flowOf()
override fun getTempMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> = override fun getTempMessagesForConversation(
flowOf() internalConversationId: String,
threadId: Long?
): Flow<List<ChatMessageEntity>> = flowOf()
override fun getTempUnsentMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> { override fun getTempUnsentMessagesForConversation(
internalConversationId: String,
threadId: Long?
): Flow<List<ChatMessageEntity>> {
// nothing to return here as long this class is only used for the Search window // nothing to return here as long this class is only used for the Search window
return flowOf() return flowOf()
} }
override fun getTempMessageForConversation( override fun getTempMessageForConversation(
internalConversationId: String, internalConversationId: String,
referenceId: String referenceId: String,
threadId: Long?
): Flow<ChatMessageEntity> = flowOf() ): Flow<ChatMessageEntity> = flowOf()
override suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>) { /* */ } override suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>) { /* */ }
@ -59,25 +68,29 @@ class DummyChatMessagesDaoImpl : ChatMessagesDao {
override fun getMessagesForConversationSince( override fun getMessagesForConversationSince(
internalConversationId: String, internalConversationId: String,
messageId: Long messageId: Long,
threadId: Long?
): Flow<List<ChatMessageEntity>> = flowOf() ): Flow<List<ChatMessageEntity>> = flowOf()
override fun getMessagesForConversationBefore( override fun getMessagesForConversationBefore(
internalConversationId: String, internalConversationId: String,
messageId: Long, messageId: Long,
limit: Int limit: Int,
threadId: Long?
): Flow<List<ChatMessageEntity>> = flowOf() ): Flow<List<ChatMessageEntity>> = flowOf()
override fun getMessagesForConversationBeforeAndEqual( override fun getMessagesForConversationBeforeAndEqual(
internalConversationId: String, internalConversationId: String,
messageId: Long, messageId: Long,
limit: Int limit: Int,
threadId: Long?
): Flow<List<ChatMessageEntity>> = flowOf() ): Flow<List<ChatMessageEntity>> = flowOf()
override fun getCountBetweenMessageIds( override fun getCountBetweenMessageIds(
internalConversationId: String, internalConversationId: String,
oldestMessageId: Long, oldestMessageId: Long,
newestMessageId: Long newestMessageId: Long,
threadId: Long?
): Int = 0 ): Int = 0
override fun clearAllMessagesForUser(pattern: String) { /* */ } override fun clearAllMessagesForUser(pattern: String) { /* */ }
@ -192,22 +205,28 @@ class DummyConversationDaoImpl : ConversationsDao {
class DummyChatBlocksDaoImpl : ChatBlocksDao { class DummyChatBlocksDaoImpl : ChatBlocksDao {
override fun deleteChatBlocks(blocks: List<ChatBlockEntity>) { /* */ } override fun deleteChatBlocks(blocks: List<ChatBlockEntity>) { /* */ }
override fun getChatBlocks(internalConversationId: String): Flow<List<ChatBlockEntity>> = flowOf() // override fun getChatBlocks(
// internalConversationId: String
// ): Flow<List<ChatBlockEntity>> = flowOf()
override fun getChatBlocksContainingMessageId( override fun getChatBlocksContainingMessageId(
internalConversationId: String, internalConversationId: String,
threadId: Long?,
messageId: Long messageId: Long
): Flow<List<ChatBlockEntity?>> = flowOf() ): Flow<List<ChatBlockEntity?>> = flowOf()
override fun getConnectedChatBlocks( override fun getConnectedChatBlocks(
internalConversationId: String, internalConversationId: String,
threadId: Long?,
oldestMessageId: Long, oldestMessageId: Long,
newestMessageId: Long newestMessageId: Long
): Flow<List<ChatBlockEntity>> = flowOf() ): Flow<List<ChatBlockEntity>> = flowOf()
override fun getNewestMessageIdFromChatBlocks(internalConversationId: String, threadId: Long?): Long = 0L
override suspend fun upsertChatBlock(chatBlock: ChatBlockEntity) { /* */ } override suspend fun upsertChatBlock(chatBlock: ChatBlockEntity) { /* */ }
override fun clearChatBlocksForUser(pattern: String) { /* */ } // override fun clearChatBlocksForUser(pattern: String) { /* */ }
override fun deleteChatBlocksOlderThan(internalConversationId: String, messageId: Long) { /* */ } override fun deleteChatBlocksOlderThan(internalConversationId: String, messageId: Long) { /* */ }
} }