mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-29 07:16:08 +00:00
fix: preserve arrival order across aliases
This commit is contained in:
parent
dfe4fba212
commit
62926f88d5
@ -99,6 +99,7 @@ object AppStateStore {
|
||||
synchronized(this) {
|
||||
if (seenMessageIds.contains(msg.id)) return
|
||||
seenMessageIds.add(msg.id)
|
||||
PrivateMessageArrivalOrder.record(msg.id)
|
||||
val conversationID = ContactDirectory.canonicalConversationId(peerID)
|
||||
val map = _privateMessages.value.toMutableMap()
|
||||
val list = (map[conversationID] ?: emptyList()) + msg
|
||||
@ -207,6 +208,7 @@ object AppStateStore {
|
||||
synchronized(this) {
|
||||
seenMessageIds.clear()
|
||||
seenPublicMessageKeys.clear()
|
||||
PrivateMessageArrivalOrder.clear()
|
||||
peerIdsByTransport.clear()
|
||||
directPeerIdsByTransport.clear()
|
||||
_peers.value = emptyList()
|
||||
|
||||
@ -103,9 +103,9 @@ object ContactDirectory {
|
||||
|
||||
return merged.mapValues { (_, messages) ->
|
||||
// A private message's timestamp comes from the sender and is not a reliable ordering
|
||||
// signal when peers' clocks differ. The lists are populated in receive order, so keep
|
||||
// their first occurrence in that same order while canonicalizing aliases.
|
||||
messages.distinctBy { it.id }
|
||||
// signal when peers' clocks differ. Use the local receipt sequence so interleaved
|
||||
// alias lists can be merged back into their global arrival order.
|
||||
PrivateMessageArrivalOrder.order(messages.distinctBy { it.id })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
package com.bitchat.android.services
|
||||
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
|
||||
/**
|
||||
* Process-local receipt sequence for private messages.
|
||||
*
|
||||
* Peer-provided timestamps cannot safely order a conversation because clocks can differ. This
|
||||
* registry follows the lifetime of [AppStateStore] and lets alias merges reconstruct the order in
|
||||
* which messages entered the app, even when they were initially stored under different keys.
|
||||
*/
|
||||
internal object PrivateMessageArrivalOrder {
|
||||
private val sequenceByMessageID = mutableMapOf<String, Long>()
|
||||
private var nextSequence = 0L
|
||||
|
||||
fun record(messageID: String) {
|
||||
synchronized(this) {
|
||||
if (messageID !in sequenceByMessageID) {
|
||||
sequenceByMessageID[messageID] = nextSequence++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun order(messages: List<BitchatMessage>): List<BitchatMessage> {
|
||||
synchronized(this) {
|
||||
if (messages.size < 2 || messages.any { it.id !in sequenceByMessageID }) {
|
||||
return messages
|
||||
}
|
||||
return messages.sortedBy { sequenceByMessageID.getValue(it.id) }
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
synchronized(this) {
|
||||
sequenceByMessageID.clear()
|
||||
nextSequence = 0L
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ package com.bitchat.android.ui
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.model.DeliveryStatus
|
||||
import com.bitchat.android.services.ContactDirectory
|
||||
import com.bitchat.android.services.PrivateMessageArrivalOrder
|
||||
import java.util.*
|
||||
import java.util.Collections
|
||||
|
||||
@ -110,6 +111,8 @@ class MessageManager(private val state: ChatState) {
|
||||
val chatMessages = currentPrivateChats[conversationID]?.toMutableList() ?: mutableListOf()
|
||||
chatMessages.add(message)
|
||||
currentPrivateChats[conversationID] = chatMessages
|
||||
// Record the local arrival sequence before canonicalizing UI aliases.
|
||||
PrivateMessageArrivalOrder.record(message.id)
|
||||
state.setPrivateChats(ContactDirectory.canonicalizePrivateChats(currentPrivateChats))
|
||||
// Reflect into process-wide store
|
||||
try { com.bitchat.android.services.AppStateStore.addPrivateMessage(conversationID, message) } catch (_: Exception) { }
|
||||
@ -132,6 +135,8 @@ class MessageManager(private val state: ChatState) {
|
||||
val chatMessages = currentPrivateChats[conversationID]?.toMutableList() ?: mutableListOf()
|
||||
chatMessages.add(message)
|
||||
currentPrivateChats[conversationID] = chatMessages
|
||||
// Record the local arrival sequence before canonicalizing UI aliases.
|
||||
PrivateMessageArrivalOrder.record(message.id)
|
||||
state.setPrivateChats(ContactDirectory.canonicalizePrivateChats(currentPrivateChats))
|
||||
// Reflect into process-wide store
|
||||
try { com.bitchat.android.services.AppStateStore.addPrivateMessage(conversationID, message) } catch (_: Exception) { }
|
||||
|
||||
@ -29,27 +29,34 @@ class ConversationAliasResolverTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `explicit alias merge keeps arrival order when peer clocks differ`() {
|
||||
fun `explicit alias merge restores global order for interleaved arrivals`() {
|
||||
val firstArrival = BitchatMessage(
|
||||
id = "first-arrival",
|
||||
id = "alias-a-first",
|
||||
sender = "alice",
|
||||
content = "sent from a clock that is ahead",
|
||||
timestamp = Date(3)
|
||||
content = "first arrival through alias A",
|
||||
timestamp = Date(300)
|
||||
)
|
||||
val secondArrival = BitchatMessage(
|
||||
id = "second-arrival",
|
||||
id = "alias-b-first",
|
||||
sender = "alice",
|
||||
content = "sent from a clock that is behind",
|
||||
timestamp = Date(1)
|
||||
content = "second arrival through alias B",
|
||||
timestamp = Date(100)
|
||||
)
|
||||
state.setPrivateChats(
|
||||
linkedMapOf(
|
||||
"target-peer" to listOf(firstArrival),
|
||||
"source-alias" to listOf(secondArrival)
|
||||
)
|
||||
val thirdArrival = BitchatMessage(
|
||||
id = "alias-a-second",
|
||||
sender = "alice",
|
||||
content = "third arrival through alias A",
|
||||
timestamp = Date(200)
|
||||
)
|
||||
AppStateStore.addPrivateMessage("target-peer", firstArrival)
|
||||
AppStateStore.addPrivateMessage("source-alias", secondArrival)
|
||||
AppStateStore.addPrivateMessage("target-peer", thirdArrival)
|
||||
state.setPrivateChats(
|
||||
linkedMapOf(
|
||||
"target-peer" to listOf(firstArrival, thirdArrival),
|
||||
"source-alias" to listOf(secondArrival)
|
||||
)
|
||||
)
|
||||
|
||||
ConversationAliasResolver.unifyChatsIntoPeer(
|
||||
state = state,
|
||||
@ -58,11 +65,11 @@ class ConversationAliasResolverTest {
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf(firstArrival, secondArrival),
|
||||
listOf(firstArrival, secondArrival, thirdArrival),
|
||||
state.getPrivateChatsValue()["target-peer"]
|
||||
)
|
||||
assertEquals(
|
||||
listOf(firstArrival, secondArrival),
|
||||
listOf(firstArrival, secondArrival, thirdArrival),
|
||||
AppStateStore.privateMessages.value["target-peer"]
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user