Merge pull request #832 from permissionlesstech/codex/fix-conversation-alias-isolation

Prevent private messages from merging across conversations
This commit is contained in:
callebtc 2026-07-30 14:54:58 +02:00 committed by GitHub
commit fa4442d836
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 61 additions and 10 deletions

View File

@ -280,11 +280,7 @@ object AppStateStore {
counts[conversationID] = (counts[conversationID] ?: 0) + 1
_unreadPrivateMessageCounts.value = counts
}
val aliases = runCatching {
ContactDirectory.aliasesForConversation(peerID) +
ContactDirectory.aliasesForConversation(conversationID) +
listOfNotNull(msg.senderPeerID)
}.getOrDefault(setOf(peerID, conversationID))
val aliases = privateConversationAliases(peerID, conversationID)
val displayName = ContactDirectory.resolve(conversationID).displayName
?: msg.sender.takeUnless {
it.isBlank() || it == "system" || it == _nickname.value
@ -320,11 +316,7 @@ object AppStateStore {
_selectedPrivateChatPeer.value
?.let(ContactDirectory::canonicalConversationId)
?.equals(conversationID, ignoreCase = true) == true
val aliases = runCatching {
ContactDirectory.aliasesForConversation(peerID) +
ContactDirectory.aliasesForConversation(conversationID) +
listOfNotNull(msg.senderPeerID)
}.getOrDefault(setOf(peerID, conversationID))
val aliases = privateConversationAliases(peerID, conversationID)
val displayName = ContactDirectory.resolve(conversationID).displayName
?: (existingMessages + msg)
.lastOrNull { candidate ->
@ -343,6 +335,20 @@ object AppStateStore {
)
}
/**
* Derive aliases from the routed conversation, never from the message author.
*
* For outgoing messages senderPeerID is our own ID, which is shared by every private chat.
* Persisting it as an alias would merge otherwise unrelated conversation histories.
*/
private fun privateConversationAliases(
peerID: String,
conversationID: String
): Set<String> = runCatching {
ContactDirectory.aliasesForConversation(peerID) +
ContactDirectory.aliasesForConversation(conversationID)
}.getOrDefault(setOf(peerID, conversationID))
fun hasSeenMessage(messageID: String): Boolean = synchronized(this) {
messageID in seenMessageIds
}

View File

@ -185,6 +185,51 @@ class IncomingMessageAdmissionTest {
runBlocking { repository.awaitPendingWrites() }
}
@Test
fun `outgoing messages to different peers keep separate persisted conversations`() {
val state = ChatState(TestScope())
state.setNickname("me")
val manager = PrivateChatManager(
state = state,
messageManager = MessageManager(state),
dataManager = DataManager(context),
noiseSessionDelegate = testNoiseDelegate()
)
runBlocking {
assertTrue(
manager.sendPrivateMessageDurably(
content = "only for alice",
peerID = "peer-a",
recipientNickname = "alice",
senderNickname = "me",
myPeerID = "self"
) { _, _, _, _ -> }
)
assertTrue(
manager.sendPrivateMessageDurably(
content = "only for bob",
peerID = "peer-b",
recipientNickname = "bob",
senderNickname = "me",
myPeerID = "self"
) { _, _, _, _ -> }
)
val alice = repository.loadConversationAndWait("peer-a")
val bob = repository.loadConversationAndWait("peer-b")
assertEquals(
listOf("only for alice"),
alice?.chats?.getValue("peer-a")?.map { it.content }
)
assertEquals(
listOf("only for bob"),
bob?.chats?.getValue("peer-b")?.map { it.content }
)
}
}
@Test
fun `outgoing callback is suppressed when durable storage fails`() {
val failingRepository = ConversationRepository(