Merge pull request #824 from permissionlesstech/codex/fix-peer-verification-status-badge

Fix peer verification status and private chat badge
This commit is contained in:
callebtc 2026-07-29 20:18:34 +02:00 committed by GitHub
commit b073dc2160
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 99 additions and 18 deletions

View File

@ -475,6 +475,19 @@ fun ConversationHeaderAction(
content = content
)
/** A read-only status slot matching the footprint of [ConversationHeaderAction]. */
@Composable
fun ConversationHeaderStatus(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Box(
modifier = modifier.size(HeaderTapTarget),
contentAlignment = Alignment.Center,
content = { content() }
)
}
@Composable
fun NicknameEditor(
value: String,

View File

@ -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, String>
): String? {
return activeMeshPeerID?.let(peerSessionStates::get)
?: peerSessionStates[conversationID]
}

View File

@ -1806,7 +1806,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)
@ -1998,8 +2002,21 @@ fun PrivateChatSheet(
)
}
// Encryption state, and the verification badge that qualifies it. Both are
// read-only for Nostr peers, which have no Noise session at all.
if (isVerified) {
ConversationHeaderStatus {
Icon(
imageVector = Icons.Filled.Verified,
contentDescription = stringResource(
R.string.fingerprint_verified_label
),
modifier = Modifier.size(HeaderIconSize),
tint = colorScheme.primary
)
}
}
// Keep the lock nearest the close action: from right to left the security
// cluster reads close, encryption, verification, then favorite.
if (!isNostrPeer && !isNostrReachableFavorite) {
ConversationHeaderAction(
onClick = { viewModel.showSecurityVerificationSheet() },
@ -2014,20 +2031,6 @@ fun PrivateChatSheet(
}
}
if (isVerified) {
Box(
modifier = Modifier.size(HeaderIconSize),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(R.drawable.ic_spec_check),
contentDescription = stringResource(R.string.verify_title),
modifier = Modifier.size(HeaderIconSize),
tint = colorScheme.primary
)
}
}
val dismiss = LocalSheetDismiss.current
CloseButton(onClick = { dismiss?.invoke() ?: onDismiss() })
}

View File

@ -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,

View File

@ -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)
}
}