diff --git a/app/src/main/java/com/bitchat/android/services/AppStateStore.kt b/app/src/main/java/com/bitchat/android/services/AppStateStore.kt index 66633fee..203e29b5 100644 --- a/app/src/main/java/com/bitchat/android/services/AppStateStore.kt +++ b/app/src/main/java/com/bitchat/android/services/AppStateStore.kt @@ -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 = runCatching { + ContactDirectory.aliasesForConversation(peerID) + + ContactDirectory.aliasesForConversation(conversationID) + }.getOrDefault(setOf(peerID, conversationID)) + fun hasSeenMessage(messageID: String): Boolean = synchronized(this) { messageID in seenMessageIds } diff --git a/app/src/test/kotlin/com/bitchat/android/services/IncomingMessageAdmissionTest.kt b/app/src/test/kotlin/com/bitchat/android/services/IncomingMessageAdmissionTest.kt index 94cdcec4..a430eb9a 100644 --- a/app/src/test/kotlin/com/bitchat/android/services/IncomingMessageAdmissionTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/services/IncomingMessageAdmissionTest.kt @@ -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(