diff --git a/app/src/main/java/com/bitchat/android/ui/ConversationSecurityState.kt b/app/src/main/java/com/bitchat/android/ui/ConversationSecurityState.kt new file mode 100644 index 00000000..5bbdbfe3 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/ui/ConversationSecurityState.kt @@ -0,0 +1,17 @@ +package com.bitchat.android.ui + +/** + * Resolves the Noise session shown for a private conversation. + * + * Persistent conversations use a canonical contact ID, while live Noise sessions are keyed by + * the currently connected mesh peer ID. Prefer that live identity and retain the conversation ID + * as a fallback for peers whose IDs are already identical. + */ +internal fun resolveConversationSessionState( + conversationID: String, + activeMeshPeerID: String?, + peerSessionStates: Map +): String? { + return activeMeshPeerID?.let(peerSessionStates::get) + ?: peerSessionStates[conversationID] +} diff --git a/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt b/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt index 6f7b8969..d4b4babc 100644 --- a/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt @@ -1858,7 +1858,11 @@ fun PrivateChatSheet( val conversationID = contactResolution.conversationID val messages = privateChats[conversationID] ?: privateChats[peerID] ?: emptyList() - val sessionState = activeMeshPeerID?.let { peerSessionStates[it] } ?: peerSessionStates[peerID] + val sessionState = resolveConversationSessionState( + conversationID = peerID, + activeMeshPeerID = activeMeshPeerID, + peerSessionStates = peerSessionStates + ) val fingerprint = activeMeshPeerID?.let { peerFingerprints[it] } ?: peerFingerprints[peerID] ?: ContactIdentityResolver.fingerprintFromContactConversationId(peerID) @@ -2072,8 +2076,10 @@ fun PrivateChatSheet( contentAlignment = Alignment.Center ) { Icon( - painter = painterResource(R.drawable.ic_spec_check), - contentDescription = stringResource(R.string.verify_title), + imageVector = Icons.Filled.Verified, + contentDescription = stringResource( + R.string.fingerprint_verified_label + ), modifier = Modifier.size(HeaderIconSize), tint = colorScheme.primary ) diff --git a/app/src/main/java/com/bitchat/android/ui/SecurityVerificationSheet.kt b/app/src/main/java/com/bitchat/android/ui/SecurityVerificationSheet.kt index 40da30be..71e59f0d 100644 --- a/app/src/main/java/com/bitchat/android/ui/SecurityVerificationSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/SecurityVerificationSheet.kt @@ -48,6 +48,7 @@ import com.bitchat.android.R import com.bitchat.android.core.ui.component.button.CloseButton import com.bitchat.android.core.ui.component.sheet.LocalSheetDismiss import com.bitchat.android.core.ui.component.sheet.BitchatBottomSheet +import com.bitchat.android.services.ContactDirectory private data class SecurityStatusInfo( val text: String, @@ -100,7 +101,12 @@ fun SecurityVerificationSheet( val displayName = viewModel.resolvePeerDisplayNameForFingerprint(selectedPeerID) val fingerprint = viewModel.getPeerFingerprintForDisplay(selectedPeerID) val isVerified = fingerprint != null && verifiedFingerprints.contains(fingerprint) - val sessionState = peerSessionStates[selectedPeerID] + val activeMeshPeerID = ContactDirectory.resolve(selectedPeerID).meshPeerID + val sessionState = resolveConversationSessionState( + conversationID = selectedPeerID, + activeMeshPeerID = activeMeshPeerID, + peerSessionStates = peerSessionStates + ) val statusInfo = buildStatusInfo( isVerified = isVerified, sessionState = sessionState, diff --git a/app/src/test/java/com/bitchat/android/ui/ConversationSecurityStateTest.kt b/app/src/test/java/com/bitchat/android/ui/ConversationSecurityStateTest.kt new file mode 100644 index 00000000..c7de0885 --- /dev/null +++ b/app/src/test/java/com/bitchat/android/ui/ConversationSecurityStateTest.kt @@ -0,0 +1,42 @@ +package com.bitchat.android.ui + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ConversationSecurityStateTest { + @Test + fun `active mesh peer session wins for canonical conversation`() { + val result = resolveConversationSessionState( + conversationID = "contact-alice", + activeMeshPeerID = "mesh-alice", + peerSessionStates = mapOf( + "contact-alice" to "uninitialized", + "mesh-alice" to "established" + ) + ) + + assertEquals("established", result) + } + + @Test + fun `conversation session is fallback when live identity has no state`() { + val result = resolveConversationSessionState( + conversationID = "contact-alice", + activeMeshPeerID = "mesh-alice", + peerSessionStates = mapOf("contact-alice" to "handshaking") + ) + + assertEquals("handshaking", result) + } + + @Test + fun `conversation session resolves when mesh and conversation IDs are identical`() { + val result = resolveConversationSessionState( + conversationID = "mesh-alice", + activeMeshPeerID = "mesh-alice", + peerSessionStates = mapOf("mesh-alice" to "established") + ) + + assertEquals("established", result) + } +}