From 054887bad8f111fa5ac1ee0e05c146e1c9b27f90 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:41:11 +0200 Subject: [PATCH] Fix request sync duplicate public messages (#707) Co-authored-by: CC --- .../bitchat/android/mesh/MessageHandler.kt | 4 +- .../android/protocol/BinaryProtocol.kt | 6 +- .../bitchat/android/services/AppStateStore.kt | 17 +++++- .../android/services/AppStateStoreTest.kt | 58 +++++++++++++++++++ 4 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 app/src/test/kotlin/com/bitchat/android/services/AppStateStoreTest.kt diff --git a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt index 63081d74..ecac572b 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -7,6 +7,7 @@ import com.bitchat.android.model.IdentityAnnouncement import com.bitchat.android.model.RoutedPacket import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.MessageType +import com.bitchat.android.sync.PacketIdUtil import com.bitchat.android.util.toHexString import kotlinx.coroutines.* import java.util.* @@ -400,7 +401,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro } val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file) val message = BitchatMessage( - id = java.util.UUID.randomUUID().toString().uppercase(), + id = PacketIdUtil.computeIdHex(packet).uppercase(), sender = delegate?.getPeerNickname(peerID) ?: "unknown", content = savedPath, type = com.bitchat.android.features.file.FileUtils.messageTypeForMime(file.mimeType), @@ -416,6 +417,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro // Fallback: plain text val message = BitchatMessage( + id = PacketIdUtil.computeIdHex(packet).uppercase(), sender = delegate?.getPeerNickname(peerID) ?: "unknown", content = String(packet.payload, Charsets.UTF_8), senderPeerID = peerID, diff --git a/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt b/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt index 882c9a75..db17ba1c 100644 --- a/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt +++ b/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt @@ -36,7 +36,7 @@ object SpecialRecipients { /** * Binary packet format - 100% backward compatible with iOS version * - * Header (13 bytes for v1, 15 bytes for v2): + * Header (14 bytes for v1, 16 bytes for v2): * - Version: 1 byte * - Type: 1 byte * - TTL: 1 byte @@ -178,8 +178,8 @@ data class BitchatPacket( * Binary Protocol implementation - supports v1 and v2, backward compatible */ object BinaryProtocol { - private const val HEADER_SIZE_V1 = 13 - private const val HEADER_SIZE_V2 = 15 + private const val HEADER_SIZE_V1 = 14 + private const val HEADER_SIZE_V2 = 16 private const val SENDER_ID_SIZE = 8 private const val RECIPIENT_ID_SIZE = 8 private const val SIGNATURE_SIZE = 64 diff --git a/app/src/main/java/com/bitchat/android/services/AppStateStore.kt b/app/src/main/java/com/bitchat/android/services/AppStateStore.kt index 07f146bd..0997beff 100644 --- a/app/src/main/java/com/bitchat/android/services/AppStateStore.kt +++ b/app/src/main/java/com/bitchat/android/services/AppStateStore.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.asStateFlow object AppStateStore { // Global de-dup set by message id to avoid duplicate keys in Compose lists private val seenMessageIds = mutableSetOf() + private val seenPublicMessageKeys = mutableSetOf() // Connected peer IDs (mesh ephemeral IDs) private val _peers = MutableStateFlow>(emptyList()) val peers: StateFlow> = _peers.asStateFlow() @@ -35,8 +36,10 @@ object AppStateStore { fun addPublicMessage(msg: BitchatMessage) { synchronized(this) { - if (seenMessageIds.contains(msg.id)) return + val publicKey = publicMessageKey(msg) + if (seenMessageIds.contains(msg.id) || seenPublicMessageKeys.contains(publicKey)) return seenMessageIds.add(msg.id) + seenPublicMessageKeys.add(publicKey) _publicMessages.value = _publicMessages.value + msg } } @@ -100,10 +103,22 @@ object AppStateStore { fun clear() { synchronized(this) { seenMessageIds.clear() + seenPublicMessageKeys.clear() _peers.value = emptyList() _publicMessages.value = emptyList() _privateMessages.value = emptyMap() _channelMessages.value = emptyMap() } } + + private fun publicMessageKey(msg: BitchatMessage): String { + val sender = msg.senderPeerID ?: msg.sender + return listOf( + sender, + msg.timestamp.time.toString(), + msg.type.name, + msg.channel ?: "", + msg.content + ).joinToString("\u001F") + } } diff --git a/app/src/test/kotlin/com/bitchat/android/services/AppStateStoreTest.kt b/app/src/test/kotlin/com/bitchat/android/services/AppStateStoreTest.kt new file mode 100644 index 00000000..3c680777 --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/services/AppStateStoreTest.kt @@ -0,0 +1,58 @@ +package com.bitchat.android.services + +import com.bitchat.android.model.BitchatMessage +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.util.Date + +class AppStateStoreTest { + @Before + fun setUp() { + AppStateStore.clear() + } + + @After + fun tearDown() { + AppStateStore.clear() + } + + @Test + fun `public timeline collapses request sync replay even when android message ids differ`() { + val timestamp = Date(1_700_000_000_000L) + val originalDelivery = BitchatMessage( + id = "random-id-from-first-delivery", + sender = "alice", + content = "hello from sync", + timestamp = timestamp, + senderPeerID = "1122334455667788" + ) + val requestSyncReplay = originalDelivery.copy(id = "different-random-id-from-replay") + + AppStateStore.addPublicMessage(originalDelivery) + AppStateStore.addPublicMessage(requestSyncReplay) + + assertEquals(listOf(originalDelivery), AppStateStore.publicMessages.value) + } + + @Test + fun `public timeline still keeps same content sent at different packet timestamps`() { + val first = BitchatMessage( + id = "first-packet-id", + sender = "alice", + content = "same text", + timestamp = Date(1_700_000_000_000L), + senderPeerID = "1122334455667788" + ) + val second = first.copy( + id = "second-packet-id", + timestamp = Date(first.timestamp.time + 1_000L) + ) + + AppStateStore.addPublicMessage(first) + AppStateStore.addPublicMessage(second) + + assertEquals(listOf(first, second), AppStateStore.publicMessages.value) + } +}