From 6f577798d405bccddb79965d50e33f3fc8a8ee72 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 13 Jan 2026 15:04:35 +0500 Subject: [PATCH] fix: Update read receipt handling to prevent automatic sending and ensure user visibility before marking messages as read --- .../messenger/database/MessageEntities.kt | 8 +- .../messenger/ui/chats/ChatDetailScreen.kt | 2 + .../messenger/ui/chats/ChatViewModel.kt | 98 +++++++++++++++---- 3 files changed, 84 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt index 9a3ac8e..04d421f 100644 --- a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt +++ b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt @@ -113,15 +113,15 @@ data class DialogEntity( interface MessageDao { /** - * Вставка нового сообщения + * Вставка нового сообщения (IGNORE если уже существует) */ - @Insert(onConflict = OnConflictStrategy.REPLACE) + @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertMessage(message: MessageEntity): Long /** - * Вставка нескольких сообщений + * Вставка нескольких сообщений (IGNORE если уже существуют) */ - @Insert(onConflict = OnConflictStrategy.REPLACE) + @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertMessages(messages: List) /** diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 968c9e9..a313087 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -371,6 +371,8 @@ fun ChatDetailScreen( onDispose { focusManager.clearFocus() keyboardController?.hide() + // 🔥 Закрываем диалог - сообщения больше не будут читаться автоматически + viewModel.closeDialog() } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt index 18d3c9d..632d31b 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt @@ -110,6 +110,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { // Флаг что read receipt уже отправлен для текущего диалога private var readReceiptSentForCurrentDialog = false + // 🔥 Флаг что диалог АКТИВЕН (пользователь внутри чата, а не на главной) + // Как currentDialogPublicKeyView в архиве + private var isDialogActive = false + init { setupPacketListeners() } @@ -292,34 +296,25 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { ProtocolManager.addLog("✅ Added to UI: ${packet.messageId.take(8)}... text: ${decryptedText.take(20)}") } - // Сохраняем в БД (INSERT OR IGNORE - не будет дублей) + // 🔥 Сохраняем в БД здесь (в ChatViewModel) + // ProtocolManager.setupPacketHandlers() не вызывается, поэтому сохраняем сами saveMessageToDatabase( messageId = packet.messageId, text = decryptedText, encryptedContent = packet.content, encryptedKey = packet.chachaKey, timestamp = packet.timestamp, - isFromMe = false, - delivered = 1, - attachmentsJson = attachmentsJson // 🔥 Сохраняем attachments + isFromMe = false, // Это входящее сообщение + delivered = DeliveryStatus.DELIVERED.value, + attachmentsJson = attachmentsJson ) - - // Обновляем диалог - saveDialog(decryptedText, packet.timestamp) - // ⚠️ Delivery отправляется в ProtocolManager.setupPacketHandlers() - // Не отправляем повторно чтобы избежать дублирования! + // 🔥 Обновляем диалог + updateDialog(opponentKey!!, decryptedText, packet.timestamp, incrementUnread = !isDialogActive) - // 👁️ Отмечаем сообщение как прочитанное в БД - messageDao.markAsRead(account, packet.messageId) - - // 👁️ Отправляем read receipt собеседнику (как в архиве - сразу при получении) - delay(100) // Небольшая задержка для естественности - withContext(Dispatchers.Main) { - // Обновляем timestamp и отправляем read receipt - lastReadMessageTimestamp = packet.timestamp - sendReadReceiptToOpponent() - } + // 👁️ НЕ отправляем read receipt автоматически! + // Read receipt отправляется только когда пользователь видит сообщение + // (через markVisibleMessagesAsRead вызываемый из ChatDetailScreen) } catch (e: Exception) { ProtocolManager.addLog("❌ Error handling incoming message: ${e.message}") @@ -381,6 +376,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { isLoadingMessages = false lastReadMessageTimestamp = 0L readReceiptSentForCurrentDialog = false + isDialogActive = true // 🔥 Диалог активен! ProtocolManager.addLog("💬 Dialog opened: ${title.ifEmpty { publicKey.take(16) }}...") @@ -391,6 +387,15 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { loadMessagesFromDatabase() } + /** + * 🔥 Закрыть диалог (вызывается когда пользователь выходит из чата) + * Как setCurrentDialogPublicKeyView("") в архиве + */ + fun closeDialog() { + isDialogActive = false + ProtocolManager.addLog("💬 Dialog closed (isDialogActive = false)") + } + /** * 🚀 Оптимизированная загрузка сообщений с пагинацией */ @@ -922,6 +927,44 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { } } + /** + * Обновить диалог при входящем сообщении + */ + private suspend fun updateDialog(opponentKey: String, lastMessage: String, timestamp: Long, incrementUnread: Boolean) { + val account = myPublicKey ?: return + + try { + val existingDialog = dialogDao.getDialog(account, opponentKey) + + if (existingDialog != null) { + // Обновляем последнее сообщение + dialogDao.updateLastMessage(account, opponentKey, lastMessage, timestamp) + + // Инкрементируем непрочитанные если нужно + if (incrementUnread) { + dialogDao.incrementUnreadCount(account, opponentKey) + ProtocolManager.addLog("📬 Unread incremented for: ${opponentKey.take(16)}...") + } + ProtocolManager.addLog("✅ Dialog updated: ${lastMessage.take(20)}...") + } else { + // Создаём новый диалог + dialogDao.insertDialog(DialogEntity( + account = account, + opponentKey = opponentKey, + opponentTitle = opponentTitle, + opponentUsername = opponentUsername, + lastMessage = lastMessage, + lastMessageTimestamp = timestamp, + unreadCount = if (incrementUnread) 1 else 0 + )) + ProtocolManager.addLog("✅ Dialog created (new)") + } + } catch (e: Exception) { + ProtocolManager.addLog("❌ updateDialog error: ${e.message}") + Log.e(TAG, "updateDialog error", e) + } + } + /** * Сохранить сообщение в базу данных */ @@ -941,8 +984,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { try { val dialogKey = getDialogKey(account, opponent) + // Проверяем существует ли сообщение + val exists = messageDao.messageExists(account, messageId) ProtocolManager.addLog("💾 Saving message to DB:") - ProtocolManager.addLog(" messageId: ${messageId.take(8)}...") + ProtocolManager.addLog(" messageId: $messageId") + ProtocolManager.addLog(" exists in DB: $exists") ProtocolManager.addLog(" dialogKey: $dialogKey") ProtocolManager.addLog(" text: ${text.take(20)}...") @@ -1033,6 +1079,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { * Означает что мы прочитали все сообщения от этого собеседника */ private fun sendReadReceiptToOpponent() { + // 🔥 Не отправляем read receipt если диалог не активен (как в архиве) + if (!isDialogActive) { + ProtocolManager.addLog("👁️ Read receipt skipped - dialog not active") + return + } + val opponent = opponentKey ?: return val sender = myPublicKey ?: return val privateKey = myPrivateKey ?: return @@ -1068,6 +1120,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { * Теперь работает как в архиве - при изменении списка сообщений */ fun markVisibleMessagesAsRead() { + // 🔥 Не читаем если диалог не активен + if (!isDialogActive) { + ProtocolManager.addLog("👁️ markVisibleMessagesAsRead skipped - dialog not active") + return + } + val opponent = opponentKey ?: return val account = myPublicKey ?: return