Fix request sync duplicate public messages (#707)

Co-authored-by: CC <cc@ggg.local>
This commit is contained in:
callebtc
2026-06-08 15:41:11 -05:00
committed by GitHub
co-authored by CC
parent f7ce1f4be6
commit 054887bad8
4 changed files with 80 additions and 5 deletions
@@ -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,
@@ -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
@@ -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<String>()
private val seenPublicMessageKeys = mutableSetOf<String>()
// Connected peer IDs (mesh ephemeral IDs)
private val _peers = MutableStateFlow<List<String>>(emptyList())
val peers: StateFlow<List<String>> = _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")
}
}
@@ -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)
}
}