mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 22:45:20 +00:00
Fix request sync duplicate public messages (#707)
Co-authored-by: CC <cc@ggg.local>
This commit is contained in:
@@ -7,6 +7,7 @@ import com.bitchat.android.model.IdentityAnnouncement
|
|||||||
import com.bitchat.android.model.RoutedPacket
|
import com.bitchat.android.model.RoutedPacket
|
||||||
import com.bitchat.android.protocol.BitchatPacket
|
import com.bitchat.android.protocol.BitchatPacket
|
||||||
import com.bitchat.android.protocol.MessageType
|
import com.bitchat.android.protocol.MessageType
|
||||||
|
import com.bitchat.android.sync.PacketIdUtil
|
||||||
import com.bitchat.android.util.toHexString
|
import com.bitchat.android.util.toHexString
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.util.*
|
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 savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
||||||
val message = BitchatMessage(
|
val message = BitchatMessage(
|
||||||
id = java.util.UUID.randomUUID().toString().uppercase(),
|
id = PacketIdUtil.computeIdHex(packet).uppercase(),
|
||||||
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
||||||
content = savedPath,
|
content = savedPath,
|
||||||
type = com.bitchat.android.features.file.FileUtils.messageTypeForMime(file.mimeType),
|
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
|
// Fallback: plain text
|
||||||
val message = BitchatMessage(
|
val message = BitchatMessage(
|
||||||
|
id = PacketIdUtil.computeIdHex(packet).uppercase(),
|
||||||
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
||||||
content = String(packet.payload, Charsets.UTF_8),
|
content = String(packet.payload, Charsets.UTF_8),
|
||||||
senderPeerID = peerID,
|
senderPeerID = peerID,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ object SpecialRecipients {
|
|||||||
/**
|
/**
|
||||||
* Binary packet format - 100% backward compatible with iOS version
|
* 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
|
* - Version: 1 byte
|
||||||
* - Type: 1 byte
|
* - Type: 1 byte
|
||||||
* - TTL: 1 byte
|
* - TTL: 1 byte
|
||||||
@@ -178,8 +178,8 @@ data class BitchatPacket(
|
|||||||
* Binary Protocol implementation - supports v1 and v2, backward compatible
|
* Binary Protocol implementation - supports v1 and v2, backward compatible
|
||||||
*/
|
*/
|
||||||
object BinaryProtocol {
|
object BinaryProtocol {
|
||||||
private const val HEADER_SIZE_V1 = 13
|
private const val HEADER_SIZE_V1 = 14
|
||||||
private const val HEADER_SIZE_V2 = 15
|
private const val HEADER_SIZE_V2 = 16
|
||||||
private const val SENDER_ID_SIZE = 8
|
private const val SENDER_ID_SIZE = 8
|
||||||
private const val RECIPIENT_ID_SIZE = 8
|
private const val RECIPIENT_ID_SIZE = 8
|
||||||
private const val SIGNATURE_SIZE = 64
|
private const val SIGNATURE_SIZE = 64
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
object AppStateStore {
|
object AppStateStore {
|
||||||
// Global de-dup set by message id to avoid duplicate keys in Compose lists
|
// Global de-dup set by message id to avoid duplicate keys in Compose lists
|
||||||
private val seenMessageIds = mutableSetOf<String>()
|
private val seenMessageIds = mutableSetOf<String>()
|
||||||
|
private val seenPublicMessageKeys = mutableSetOf<String>()
|
||||||
// Connected peer IDs (mesh ephemeral IDs)
|
// Connected peer IDs (mesh ephemeral IDs)
|
||||||
private val _peers = MutableStateFlow<List<String>>(emptyList())
|
private val _peers = MutableStateFlow<List<String>>(emptyList())
|
||||||
val peers: StateFlow<List<String>> = _peers.asStateFlow()
|
val peers: StateFlow<List<String>> = _peers.asStateFlow()
|
||||||
@@ -35,8 +36,10 @@ object AppStateStore {
|
|||||||
|
|
||||||
fun addPublicMessage(msg: BitchatMessage) {
|
fun addPublicMessage(msg: BitchatMessage) {
|
||||||
synchronized(this) {
|
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)
|
seenMessageIds.add(msg.id)
|
||||||
|
seenPublicMessageKeys.add(publicKey)
|
||||||
_publicMessages.value = _publicMessages.value + msg
|
_publicMessages.value = _publicMessages.value + msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,10 +103,22 @@ object AppStateStore {
|
|||||||
fun clear() {
|
fun clear() {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
seenMessageIds.clear()
|
seenMessageIds.clear()
|
||||||
|
seenPublicMessageKeys.clear()
|
||||||
_peers.value = emptyList()
|
_peers.value = emptyList()
|
||||||
_publicMessages.value = emptyList()
|
_publicMessages.value = emptyList()
|
||||||
_privateMessages.value = emptyMap()
|
_privateMessages.value = emptyMap()
|
||||||
_channelMessages.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user