fix: Update read receipt handling to prevent automatic sending and ensure user visibility before marking messages as read

This commit is contained in:
k1ngsterr1
2026-01-13 15:04:35 +05:00
parent eb8d24a782
commit 6f577798d4
3 changed files with 84 additions and 24 deletions

View File

@@ -113,15 +113,15 @@ data class DialogEntity(
interface MessageDao { interface MessageDao {
/** /**
* Вставка нового сообщения * Вставка нового сообщения (IGNORE если уже существует)
*/ */
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertMessage(message: MessageEntity): Long suspend fun insertMessage(message: MessageEntity): Long
/** /**
* Вставка нескольких сообщений * Вставка нескольких сообщений (IGNORE если уже существуют)
*/ */
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertMessages(messages: List<MessageEntity>) suspend fun insertMessages(messages: List<MessageEntity>)
/** /**

View File

@@ -371,6 +371,8 @@ fun ChatDetailScreen(
onDispose { onDispose {
focusManager.clearFocus() focusManager.clearFocus()
keyboardController?.hide() keyboardController?.hide()
// 🔥 Закрываем диалог - сообщения больше не будут читаться автоматически
viewModel.closeDialog()
} }
} }

View File

@@ -110,6 +110,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// Флаг что read receipt уже отправлен для текущего диалога // Флаг что read receipt уже отправлен для текущего диалога
private var readReceiptSentForCurrentDialog = false private var readReceiptSentForCurrentDialog = false
// 🔥 Флаг что диалог АКТИВЕН (пользователь внутри чата, а не на главной)
// Как currentDialogPublicKeyView в архиве
private var isDialogActive = false
init { init {
setupPacketListeners() setupPacketListeners()
} }
@@ -292,34 +296,25 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
ProtocolManager.addLog("✅ Added to UI: ${packet.messageId.take(8)}... text: ${decryptedText.take(20)}") ProtocolManager.addLog("✅ Added to UI: ${packet.messageId.take(8)}... text: ${decryptedText.take(20)}")
} }
// Сохраняем в БД (INSERT OR IGNORE - не будет дублей) // 🔥 Сохраняем в БД здесь (в ChatViewModel)
// ProtocolManager.setupPacketHandlers() не вызывается, поэтому сохраняем сами
saveMessageToDatabase( saveMessageToDatabase(
messageId = packet.messageId, messageId = packet.messageId,
text = decryptedText, text = decryptedText,
encryptedContent = packet.content, encryptedContent = packet.content,
encryptedKey = packet.chachaKey, encryptedKey = packet.chachaKey,
timestamp = packet.timestamp, timestamp = packet.timestamp,
isFromMe = false, isFromMe = false, // Это входящее сообщение
delivered = 1, delivered = DeliveryStatus.DELIVERED.value,
attachmentsJson = attachmentsJson // 🔥 Сохраняем attachments attachmentsJson = attachmentsJson
) )
// Обновляем диалог // 🔥 Обновляем диалог
saveDialog(decryptedText, packet.timestamp) updateDialog(opponentKey!!, decryptedText, packet.timestamp, incrementUnread = !isDialogActive)
// Delivery отправляется в ProtocolManager.setupPacketHandlers() // 👁 НЕ отправляем read receipt автоматически!
// Не отправляем повторно чтобы избежать дублирования! // Read receipt отправляется только когда пользователь видит сообщение
// (через markVisibleMessagesAsRead вызываемый из ChatDetailScreen)
// 👁️ Отмечаем сообщение как прочитанное в БД
messageDao.markAsRead(account, packet.messageId)
// 👁️ Отправляем read receipt собеседнику (как в архиве - сразу при получении)
delay(100) // Небольшая задержка для естественности
withContext(Dispatchers.Main) {
// Обновляем timestamp и отправляем read receipt
lastReadMessageTimestamp = packet.timestamp
sendReadReceiptToOpponent()
}
} catch (e: Exception) { } catch (e: Exception) {
ProtocolManager.addLog("❌ Error handling incoming message: ${e.message}") ProtocolManager.addLog("❌ Error handling incoming message: ${e.message}")
@@ -381,6 +376,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
isLoadingMessages = false isLoadingMessages = false
lastReadMessageTimestamp = 0L lastReadMessageTimestamp = 0L
readReceiptSentForCurrentDialog = false readReceiptSentForCurrentDialog = false
isDialogActive = true // 🔥 Диалог активен!
ProtocolManager.addLog("💬 Dialog opened: ${title.ifEmpty { publicKey.take(16) }}...") ProtocolManager.addLog("💬 Dialog opened: ${title.ifEmpty { publicKey.take(16) }}...")
@@ -391,6 +387,15 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
loadMessagesFromDatabase() 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 { try {
val dialogKey = getDialogKey(account, opponent) val dialogKey = getDialogKey(account, opponent)
// Проверяем существует ли сообщение
val exists = messageDao.messageExists(account, messageId)
ProtocolManager.addLog("💾 Saving message to DB:") 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(" dialogKey: $dialogKey")
ProtocolManager.addLog(" text: ${text.take(20)}...") ProtocolManager.addLog(" text: ${text.take(20)}...")
@@ -1033,6 +1079,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
* Означает что мы прочитали все сообщения от этого собеседника * Означает что мы прочитали все сообщения от этого собеседника
*/ */
private fun sendReadReceiptToOpponent() { private fun sendReadReceiptToOpponent() {
// 🔥 Не отправляем read receipt если диалог не активен (как в архиве)
if (!isDialogActive) {
ProtocolManager.addLog("👁️ Read receipt skipped - dialog not active")
return
}
val opponent = opponentKey ?: return val opponent = opponentKey ?: return
val sender = myPublicKey ?: return val sender = myPublicKey ?: return
val privateKey = myPrivateKey ?: return val privateKey = myPrivateKey ?: return
@@ -1068,6 +1120,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
* Теперь работает как в архиве - при изменении списка сообщений * Теперь работает как в архиве - при изменении списка сообщений
*/ */
fun markVisibleMessagesAsRead() { fun markVisibleMessagesAsRead() {
// 🔥 Не читаем если диалог не активен
if (!isDialogActive) {
ProtocolManager.addLog("👁️ markVisibleMessagesAsRead skipped - dialog not active")
return
}
val opponent = opponentKey ?: return val opponent = opponentKey ?: return
val account = myPublicKey ?: return val account = myPublicKey ?: return