diff --git a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt index ea21fbd..f4845c9 100644 --- a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt +++ b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt @@ -262,16 +262,20 @@ class MessageRepository private constructor(private val context: Context) { /** * Обработка прочтения + * В Desktop PacketRead сообщает что собеседник прочитал наши сообщения + * fromPublicKey - кто прочитал (собеседник) */ suspend fun handleRead(packet: PacketRead) { val account = currentAccount ?: return - messageDao.markAsRead(account, packet.messageId) - // Обновляем кэш + // Отмечаем все наши исходящие сообщения к этому собеседнику как прочитанные + messageDao.markAllAsRead(account, packet.fromPublicKey) + + // Обновляем кэш - все исходящие сообщения помечаем как прочитанные val dialogKey = getDialogKey(packet.fromPublicKey) messageCache[dialogKey]?.let { flow -> flow.value = flow.value.map { msg -> - if (msg.messageId == packet.messageId) msg.copy(isRead = true) + if (msg.isFromMe && !msg.isRead) msg.copy(isRead = true) else msg } } 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 cc1e129..909fc4f 100644 --- a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt +++ b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt @@ -221,6 +221,16 @@ interface MessageDao { */ @Query("SELECT EXISTS(SELECT 1 FROM messages WHERE account = :account AND message_id = :messageId)") suspend fun messageExists(account: String, messageId: String): Boolean + + /** + * Отметить все исходящие сообщения к собеседнику как прочитанные (delivered=3) + * Используется когда приходит PacketRead от собеседника + */ + @Query(""" + UPDATE messages SET delivered = 3 + WHERE account = :account AND to_public_key = :opponent AND from_me = 1 AND delivered < 3 + """) + suspend fun markAllAsRead(account: String, opponent: String) } /** diff --git a/app/src/main/java/com/rosetta/messenger/network/Packets.kt b/app/src/main/java/com/rosetta/messenger/network/Packets.kt index 05d15e4..1dd0fa8 100644 --- a/app/src/main/java/com/rosetta/messenger/network/Packets.kt +++ b/app/src/main/java/com/rosetta/messenger/network/Packets.kt @@ -335,17 +335,18 @@ class PacketMessage : Packet() { /** * Read packet (ID: 0x07) * Уведомление о прочтении сообщения + * Порядок полей как в Desktop: privateKey, fromPublicKey, toPublicKey */ class PacketRead : Packet() { - var messageId: String = "" + var privateKey: String = "" var fromPublicKey: String = "" var toPublicKey: String = "" - var privateKey: String = "" override fun getPacketId(): Int = 0x07 override fun receive(stream: Stream) { - messageId = stream.readString() + // Desktop: privateKey, fromPublicKey, toPublicKey + privateKey = stream.readString() fromPublicKey = stream.readString() toPublicKey = stream.readString() } @@ -353,10 +354,10 @@ class PacketRead : Packet() { override fun send(): Stream { val stream = Stream() stream.writeInt16(getPacketId()) - stream.writeString(messageId) + // Desktop: privateKey, fromPublicKey, toPublicKey + stream.writeString(privateKey) stream.writeString(fromPublicKey) stream.writeString(toPublicKey) - stream.writeString(privateKey) return stream } } @@ -422,6 +423,7 @@ class PacketTyping : Packet() { /** * Chunk packet (ID: 0x09) * Для разбиения больших пакетов на части (как в Desktop) + * ВАЖНО: chunkIndex и totalChunks - Int16, не Int32! */ class PacketChunk : Packet() { var chunkId: String = "" @@ -432,18 +434,20 @@ class PacketChunk : Packet() { override fun getPacketId(): Int = 0x09 override fun receive(stream: Stream) { + // В Desktop: readInt16 для index и total + chunkIndex = stream.readInt16() + totalChunks = stream.readInt16() chunkId = stream.readString() - chunkIndex = stream.readInt32() - totalChunks = stream.readInt32() data = stream.readBytes() } override fun send(): Stream { val stream = Stream() stream.writeInt16(getPacketId()) + // В Desktop: writeInt16 для index и total + stream.writeInt16(chunkIndex) + stream.writeInt16(totalChunks) stream.writeString(chunkId) - stream.writeInt32(chunkIndex) - stream.writeInt32(totalChunks) stream.writeBytes(data) return stream } diff --git a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt index 693e705..9be5bb3 100644 --- a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt @@ -87,9 +87,10 @@ object ProtocolManager { } // Обработчик прочтения (0x07) + // В Desktop PacketRead не содержит messageId - сообщает что собеседник прочитал сообщения waitPacket(0x07) { packet -> val readPacket = packet as PacketRead - addLog("✓✓ Read: ${readPacket.messageId.take(16)}...") + addLog("✓✓ Read from: ${readPacket.fromPublicKey.take(16)}...") scope.launch { messageRepository?.handleRead(readPacket) 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 71b57e1..4101567 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 @@ -134,17 +134,28 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { } } - // Прочитано + // Прочитано - пакет сообщает что собеседник прочитал наши сообщения + // В Desktop нет messageId - просто отмечаем все исходящие сообщения как прочитанные ProtocolManager.waitPacket(0x07) { packet -> val readPacket = packet as PacketRead viewModelScope.launch(Dispatchers.IO) { - // Обновляем в БД - updateMessageStatusInDb(readPacket.messageId, 3) // READ - // Обновляем UI - withContext(Dispatchers.Main) { - updateMessageStatus(readPacket.messageId, MessageStatus.READ) + // Если fromPublicKey == наш собеседник, значит он прочитал наши сообщения + if (readPacket.fromPublicKey == opponentKey) { + // Обновляем все непрочитанные исходящие сообщения в БД + val account = myPublicKey ?: return@launch + val opponent = opponentKey ?: return@launch + messageDao.markAllAsRead(account, opponent) + + // Обновляем UI - все исходящие сообщения помечаем как прочитанные + withContext(Dispatchers.Main) { + _messages.value = _messages.value.map { msg -> + if (msg.isOutgoing && msg.status != MessageStatus.READ) { + msg.copy(status = MessageStatus.READ) + } else msg + } + } + ProtocolManager.addLog("✓✓ Read receipt from: ${readPacket.fromPublicKey.take(8)}...") } - ProtocolManager.addLog("✓✓ Read: ${readPacket.messageId.take(8)}...") } } @@ -762,29 +773,31 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { /** * 👁️ Отправить подтверждение о прочтении сообщения + * В Desktop PacketRead не содержит messageId - он просто сообщает что мы прочитали сообщения */ fun sendReadReceipt(messageId: String, senderPublicKey: String) { - // Не отправляем повторно - if (sentReadReceipts.contains(messageId)) return + // Не отправляем повторно для этого собеседника + val receiptKey = senderPublicKey + if (sentReadReceipts.contains(receiptKey)) return val sender = myPublicKey ?: return val privateKey = myPrivateKey ?: return - sentReadReceipts.add(messageId) + sentReadReceipts.add(receiptKey) viewModelScope.launch(Dispatchers.IO) { try { val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey) + // Desktop формат: privateKey, fromPublicKey, toPublicKey val packet = PacketRead().apply { - this.messageId = messageId - fromPublicKey = sender - toPublicKey = senderPublicKey this.privateKey = privateKeyHash + fromPublicKey = sender // Мы (кто прочитал) + toPublicKey = senderPublicKey // Кому отправляем уведомление } ProtocolManager.send(packet) - ProtocolManager.addLog("👁️ Read receipt sent for: ${messageId.take(8)}...") + ProtocolManager.addLog("👁️ Read receipt sent to: ${senderPublicKey.take(8)}...") // Обновляем в БД что сообщение прочитано updateMessageReadInDb(messageId)