fix: preserve private message arrival order

This commit is contained in:
callebtc 2026-07-27 22:50:27 +02:00
parent d814ac7f80
commit dfe4fba212
4 changed files with 92 additions and 10 deletions

View File

@ -102,9 +102,10 @@ object ContactDirectory {
} }
return merged.mapValues { (_, messages) -> return merged.mapValues { (_, messages) ->
messages // A private message's timestamp comes from the sender and is not a reliable ordering
.distinctBy { it.id } // signal when peers' clocks differ. The lists are populated in receive order, so keep
.sortedWith(compareBy<BitchatMessage> { it.timestamp.time }.thenBy { it.id }) // their first occurrence in that same order while canonicalizing aliases.
messages.distinctBy { it.id }
} }
} }

View File

@ -65,7 +65,6 @@ object ConversationAliasResolver {
if (didMerge) { if (didMerge) {
currentChats[targetConversationID] = targetList currentChats[targetConversationID] = targetList
.distinctBy { it.id } .distinctBy { it.id }
.sortedBy { it.timestamp.time }
state.setPrivateChats(ContactDirectory.canonicalizePrivateChats(currentChats)) state.setPrivateChats(ContactDirectory.canonicalizePrivateChats(currentChats))
// Move unread flags // Move unread flags

View File

@ -128,16 +128,29 @@ class AppStateStoreTest {
} }
@Test @Test
fun `canonicalized private chat history is chronological after alias merge`() { fun `canonicalized private chat history keeps arrival order when peer clocks differ`() {
val noiseKeyHex = "01".repeat(32) val noiseKeyHex = "01".repeat(32)
val contactID = ContactIdentityResolver.contactConversationIdForNoiseKey(ByteArray(32) { 1 }) val contactID = ContactIdentityResolver.contactConversationIdForNoiseKey(ByteArray(32) { 1 })
val later = BitchatMessage(id = "later", sender = "alice", content = "later", timestamp = Date(3)) val firstArrival = BitchatMessage(
val earlier = BitchatMessage(id = "earlier", sender = "alice", content = "earlier", timestamp = Date(1)) id = "first-arrival",
sender = "alice",
content = "sent from a clock that is ahead",
timestamp = Date(3)
)
val secondArrival = BitchatMessage(
id = "second-arrival",
sender = "alice",
content = "sent from a clock that is behind",
timestamp = Date(1)
)
AppStateStore.addPrivateMessage(contactID, later) AppStateStore.addPrivateMessage(contactID, firstArrival)
AppStateStore.addPrivateMessage(noiseKeyHex, earlier) AppStateStore.addPrivateMessage(noiseKeyHex, secondArrival)
assertEquals(listOf(earlier, later), AppStateStore.privateMessages.value[contactID]) assertEquals(
listOf(firstArrival, secondArrival),
AppStateStore.privateMessages.value[contactID]
)
} }
@Test @Test

View File

@ -0,0 +1,69 @@
package com.bitchat.android.services
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.ui.ChatState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import java.util.Date
class ConversationAliasResolverTest {
private lateinit var scope: CoroutineScope
private lateinit var state: ChatState
@Before
fun setUp() {
AppStateStore.clear()
scope = CoroutineScope(SupervisorJob())
state = ChatState(scope)
}
@After
fun tearDown() {
scope.cancel()
AppStateStore.clear()
}
@Test
fun `explicit alias merge keeps arrival order when peer clocks differ`() {
val firstArrival = BitchatMessage(
id = "first-arrival",
sender = "alice",
content = "sent from a clock that is ahead",
timestamp = Date(3)
)
val secondArrival = BitchatMessage(
id = "second-arrival",
sender = "alice",
content = "sent from a clock that is behind",
timestamp = Date(1)
)
state.setPrivateChats(
linkedMapOf(
"target-peer" to listOf(firstArrival),
"source-alias" to listOf(secondArrival)
)
)
AppStateStore.addPrivateMessage("target-peer", firstArrival)
AppStateStore.addPrivateMessage("source-alias", secondArrival)
ConversationAliasResolver.unifyChatsIntoPeer(
state = state,
targetPeerID = "target-peer",
keysToMerge = listOf("source-alias")
)
assertEquals(
listOf(firstArrival, secondArrival),
state.getPrivateChatsValue()["target-peer"]
)
assertEquals(
listOf(firstArrival, secondArrival),
AppStateStore.privateMessages.value["target-peer"]
)
}
}