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 {
/**
* Вставка нового сообщения
* Вставка нового сообщения (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<MessageEntity>)
/**

View File

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

View File

@@ -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