From 339c5739d111dd4fff44d1b26dbd77bc5616f62c Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Fri, 4 Sep 2026 10:36:36 +0200 Subject: [PATCH] fix(threads): show new thread immediately instead of only after reopening chat Starting a new thread showed the message as a normal chat message until the chat was reopened. Two gaps caused this: - OfflineFirstChatRepository.sendChatMessage() discarded the server's response after a successful send and only updated the temp message's sendStatus, so the correct isThread/threadId/threadTitle from the server was never persisted until a later resync (e.g. on reopening the chat). Now the authoritative response is persisted right away, replacing the temp row. - The optimistic temp message itself did not carry isThread/ threadTitle at all, and ChatViewModel.handleThreadMessages() misclassified that temp message (isThread=true, threadId=null, not yet confirmed by the server) as a thread reply and filtered it out of the normal chat view. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../talk/chat/data/ChatMessageRepository.kt | 4 +- .../network/OfflineFirstChatRepository.kt | 39 ++++++++++--------- .../talk/chat/viewmodels/ChatViewModel.kt | 1 + .../chat/viewmodels/MessageInputViewModel.kt | 3 +- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt index c50c84e8aad..05664c24514 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt @@ -142,12 +142,14 @@ interface ChatMessageRepository : LifecycleAwareManager { referenceId: String ): Flow> + @Suppress("LongParameterList") suspend fun addTemporaryMessage( message: CharSequence, displayName: String, replyTo: Int, sendWithoutNotification: Boolean, - referenceId: String + referenceId: String, + threadTitle: String? ): Flow> @Suppress("LongParameterList") 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 70d515a795c..ce874de7745 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 @@ -608,22 +608,17 @@ class OfflineFirstChatRepository @Inject constructor( threadTitle ) - val chatMessageModel = response.ocs?.data?.toDomainModel() - - val sentMessage = if (this@OfflineFirstChatRepository::internalConversationId.isInitialized) { - chatDao - .getTempMessageForConversation( - internalConversationId, - referenceId, - threadId - ).firstOrNull() - } else { - null - } + val messageJson = response.ocs?.data + val chatMessageModel = messageJson?.toDomainModel() - sentMessage?.let { - it.sendStatus = SendStatus.SENT_PENDING_ACK - chatDao.updateChatMessage(it) + if (messageJson != null && this@OfflineFirstChatRepository::internalConversationId.isInitialized) { + // Persist the authoritative response immediately (correct isThread/threadId/threadTitle + // included) instead of leaving the client-side guess in the temp row until the next sync - + // otherwise a thread indicator only appears after reopening the chat. + chatDao.upsertChatMessagesAndDeleteTemp( + internalConversationId, + listOf(messageJson.asEntity(currentUser.id!!)) + ) } Log.d(TAG, "sending chat message succeeded: " + message) @@ -691,13 +686,14 @@ class OfflineFirstChatRepository @Inject constructor( } } - @Suppress("Detekt.TooGenericExceptionCaught") + @Suppress("Detekt.TooGenericExceptionCaught", "LongParameterList") override suspend fun addTemporaryMessage( message: CharSequence, displayName: String, replyTo: Int, sendWithoutNotification: Boolean, - referenceId: String + referenceId: String, + threadTitle: String? ): Flow> = flow { try { @@ -706,7 +702,8 @@ class OfflineFirstChatRepository @Inject constructor( message.toString(), replyTo, sendWithoutNotification, - referenceId + referenceId, + threadTitle ) chatDao.upsertChatMessage(tempChatMessageEntity) } catch (e: Exception) { @@ -1037,12 +1034,14 @@ class OfflineFirstChatRepository @Inject constructor( emit(Result.failure(e)) } + @Suppress("LongParameterList") private fun createChatMessageEntity( internalConversationId: String, message: String, replyTo: Int, sendWithoutNotification: Boolean, - referenceId: String + referenceId: String, + threadTitle: String? ): ChatMessageEntity { val currentTimeMillis = System.currentTimeMillis() @@ -1063,6 +1062,8 @@ class OfflineFirstChatRepository @Inject constructor( internalConversationId = internalConversationId, id = negativeTimeOfDayMillis, threadId = threadId, + isThread = threadTitle != null, + threadTitle = threadTitle, message = message, deleted = false, token = conversationModel.token, diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index fe4c48e6b1b..8c660c9b974 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -1676,6 +1676,7 @@ class ChatViewModel @AssistedInject constructor( private fun handleThreadMessages(chatMessageList: List): List { fun isThreadChildMessage(currentMessage: MutableMap.MutableEntry): Boolean = currentMessage.value.isThread && + currentMessage.value.threadId != null && currentMessage.value.threadId?.toInt() != currentMessage.value.jsonMessageId val chatMessageMap = chatMessageList.associateBy { it.jsonMessageId }.toMutableMap() diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt index e6128411dea..45519567509 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt @@ -166,7 +166,8 @@ class MessageInputViewModel : displayName, replyTo, sendWithoutNotification, - referenceId + referenceId, + threadTitle ).collect { result -> if (result.isSuccess) { Log.d(TAG, "temp message ref id: " + (result.getOrNull()?.referenceId ?: "none"))