mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-26 19:05:26 +00:00
Show verified badges in sidebar and add strings
This commit is contained in:
@@ -23,6 +23,7 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||||
|
import com.bitchat.android.util.hexEncodedString
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,6 +35,7 @@ import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
|||||||
fun SidebarOverlay(
|
fun SidebarOverlay(
|
||||||
viewModel: ChatViewModel,
|
viewModel: ChatViewModel,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
|
onShowVerification: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
@@ -131,6 +133,7 @@ fun SidebarOverlay(
|
|||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
selectedPrivatePeer = selectedPrivatePeer,
|
selectedPrivatePeer = selectedPrivatePeer,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
|
onShowVerification = onShowVerification,
|
||||||
onPrivateChatStart = { peerID ->
|
onPrivateChatStart = { peerID ->
|
||||||
viewModel.startPrivateChat(peerID)
|
viewModel.startPrivateChat(peerID)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
@@ -257,8 +260,11 @@ fun PeopleSection(
|
|||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme,
|
||||||
selectedPrivatePeer: String?,
|
selectedPrivatePeer: String?,
|
||||||
viewModel: ChatViewModel,
|
viewModel: ChatViewModel,
|
||||||
|
onShowVerification: () -> Unit,
|
||||||
onPrivateChatStart: (String) -> Unit
|
onPrivateChatStart: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
|
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -279,6 +285,17 @@ fun PeopleSection(
|
|||||||
color = colorScheme.onSurface.copy(alpha = 0.6f),
|
color = colorScheme.onSurface.copy(alpha = 0.6f),
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
if (selectedLocationChannel !is com.bitchat.android.geohash.ChannelID.Location) {
|
||||||
|
IconButton(onClick = onShowVerification, modifier = Modifier.size(24.dp)) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.QrCode,
|
||||||
|
contentDescription = stringResource(R.string.verify_title),
|
||||||
|
tint = colorScheme.onSurface.copy(alpha = 0.8f),
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectedPeers.isEmpty()) {
|
if (connectedPeers.isEmpty()) {
|
||||||
@@ -295,6 +312,7 @@ fun PeopleSection(
|
|||||||
val privateChats by viewModel.privateChats.collectAsStateWithLifecycle()
|
val privateChats by viewModel.privateChats.collectAsStateWithLifecycle()
|
||||||
val favoritePeers by viewModel.favoritePeers.collectAsStateWithLifecycle()
|
val favoritePeers by viewModel.favoritePeers.collectAsStateWithLifecycle()
|
||||||
val peerFingerprints by viewModel.peerFingerprints.collectAsStateWithLifecycle()
|
val peerFingerprints by viewModel.peerFingerprints.collectAsStateWithLifecycle()
|
||||||
|
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Reactive favorite computation for all peers
|
// Reactive favorite computation for all peers
|
||||||
val peerFavoriteStates = remember(favoritePeers, peerFingerprints, connectedPeers) {
|
val peerFavoriteStates = remember(favoritePeers, peerFingerprints, connectedPeers) {
|
||||||
@@ -308,10 +326,16 @@ fun PeopleSection(
|
|||||||
// Build mapping of connected peerID -> noise key hex to unify with offline favorites
|
// Build mapping of connected peerID -> noise key hex to unify with offline favorites
|
||||||
val noiseHexByPeerID: Map<String, String> = connectedPeers.associateWith { pid ->
|
val noiseHexByPeerID: Map<String, String> = connectedPeers.associateWith { pid ->
|
||||||
try {
|
try {
|
||||||
viewModel.meshService.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
viewModel.meshService.getPeerInfo(pid)?.noisePublicKey?.hexEncodedString()
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}.filterValues { it != null }.mapValues { it.value!! }
|
}.filterValues { it != null }.mapValues { it.value!! }
|
||||||
|
|
||||||
|
val peerVerifiedStates = remember(verifiedFingerprints, peerFingerprints, connectedPeers) {
|
||||||
|
connectedPeers.associateWith { peerID ->
|
||||||
|
viewModel.isPeerVerified(peerID, verifiedFingerprints)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Log.d("SidebarComponents", "Recomposing with ${favoritePeers.size} favorites, peer states: $peerFavoriteStates")
|
Log.d("SidebarComponents", "Recomposing with ${favoritePeers.size} favorites, peer states: $peerFavoriteStates")
|
||||||
|
|
||||||
// Smart sorting: unread DMs first, then by most recent DM, then favorites, then alphabetical
|
// Smart sorting: unread DMs first, then by most recent DM, then favorites, then alphabetical
|
||||||
@@ -342,7 +366,7 @@ fun PeopleSection(
|
|||||||
// Offline favorites (exclude ones mapped to connected)
|
// Offline favorites (exclude ones mapped to connected)
|
||||||
val offlineFavorites = com.bitchat.android.favorites.FavoritesPersistenceService.shared.getOurFavorites()
|
val offlineFavorites = com.bitchat.android.favorites.FavoritesPersistenceService.shared.getOurFavorites()
|
||||||
offlineFavorites.forEach { fav ->
|
offlineFavorites.forEach { fav ->
|
||||||
val favPeerID = fav.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
|
val favPeerID = fav.peerNoisePublicKey.hexEncodedString()
|
||||||
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
|
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
|
||||||
if (!isMappedToConnected) {
|
if (!isMappedToConnected) {
|
||||||
val dn = peerNicknames[favPeerID] ?: fav.peerNickname
|
val dn = peerNicknames[favPeerID] ?: fav.peerNickname
|
||||||
@@ -368,6 +392,7 @@ fun PeopleSection(
|
|||||||
|
|
||||||
sortedPeers.forEach { peerID ->
|
sortedPeers.forEach { peerID ->
|
||||||
val isFavorite = peerFavoriteStates[peerID] ?: false
|
val isFavorite = peerFavoriteStates[peerID] ?: false
|
||||||
|
val isVerified = peerVerifiedStates[peerID] ?: false
|
||||||
// fingerprint and favorite relationship resolution not needed here; UI will show Nostr globe for appended offline favorites below
|
// fingerprint and favorite relationship resolution not needed here; UI will show Nostr globe for appended offline favorites below
|
||||||
|
|
||||||
val noiseHex = noiseHexByPeerID[peerID]
|
val noiseHex = noiseHexByPeerID[peerID]
|
||||||
@@ -392,6 +417,7 @@ fun PeopleSection(
|
|||||||
isDirect = isDirectLive,
|
isDirect = isDirectLive,
|
||||||
isSelected = peerID == selectedPrivatePeer,
|
isSelected = peerID == selectedPrivatePeer,
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
|
isVerified = isVerified,
|
||||||
hasUnreadDM = combinedHasUnread,
|
hasUnreadDM = combinedHasUnread,
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
@@ -408,7 +434,7 @@ fun PeopleSection(
|
|||||||
|
|
||||||
// Append offline favorites we actively favorite (and not currently connected)
|
// Append offline favorites we actively favorite (and not currently connected)
|
||||||
offlineFavorites.forEach { fav ->
|
offlineFavorites.forEach { fav ->
|
||||||
val favPeerID = fav.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
|
val favPeerID = fav.peerNoisePublicKey.hexEncodedString()
|
||||||
// If any connected peer maps to this noise key, skip showing the offline entry
|
// If any connected peer maps to this noise key, skip showing the offline entry
|
||||||
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
|
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
|
||||||
if (isMappedToConnected) return@forEach
|
if (isMappedToConnected) return@forEach
|
||||||
@@ -419,7 +445,7 @@ fun PeopleSection(
|
|||||||
if (npubOrHex != null) {
|
if (npubOrHex != null) {
|
||||||
val hex = if (npubOrHex.startsWith("npub")) {
|
val hex = if (npubOrHex.startsWith("npub")) {
|
||||||
val (hrp, data) = com.bitchat.android.nostr.Bech32.decode(npubOrHex)
|
val (hrp, data) = com.bitchat.android.nostr.Bech32.decode(npubOrHex)
|
||||||
if (hrp == "npub") data.joinToString("") { "%02x".format(it) } else null
|
if (hrp == "npub") data.hexEncodedString() else null
|
||||||
} else {
|
} else {
|
||||||
npubOrHex.lowercase()
|
npubOrHex.lowercase()
|
||||||
}
|
}
|
||||||
@@ -435,6 +461,7 @@ fun PeopleSection(
|
|||||||
val dn = peerNicknames[favPeerID] ?: fav.peerNickname
|
val dn = peerNicknames[favPeerID] ?: fav.peerNickname
|
||||||
val (bName, _) = com.bitchat.android.ui.splitSuffix(dn)
|
val (bName, _) = com.bitchat.android.ui.splitSuffix(dn)
|
||||||
val showHash = (baseNameCounts[bName] ?: 0) > 1
|
val showHash = (baseNameCounts[bName] ?: 0) > 1
|
||||||
|
val isVerified = viewModel.isNoisePublicKeyVerified(fav.peerNoisePublicKey, verifiedFingerprints)
|
||||||
|
|
||||||
// Compute unreadCount from either noise conversation or Nostr conversation
|
// Compute unreadCount from either noise conversation or Nostr conversation
|
||||||
val unreadCount = (
|
val unreadCount = (
|
||||||
@@ -449,6 +476,7 @@ fun PeopleSection(
|
|||||||
isDirect = false,
|
isDirect = false,
|
||||||
isSelected = (mappedConnectedPeerID ?: favPeerID) == selectedPrivatePeer,
|
isSelected = (mappedConnectedPeerID ?: favPeerID) == selectedPrivatePeer,
|
||||||
isFavorite = true,
|
isFavorite = true,
|
||||||
|
isVerified = isVerified,
|
||||||
hasUnreadDM = hasUnread,
|
hasUnreadDM = hasUnread,
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
@@ -516,6 +544,7 @@ private fun PeerItem(
|
|||||||
isDirect: Boolean,
|
isDirect: Boolean,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
isFavorite: Boolean,
|
isFavorite: Boolean,
|
||||||
|
isVerified: Boolean,
|
||||||
hasUnreadDM: Boolean,
|
hasUnreadDM: Boolean,
|
||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme,
|
||||||
viewModel: ChatViewModel,
|
viewModel: ChatViewModel,
|
||||||
@@ -616,6 +645,16 @@ private fun PeerItem(
|
|||||||
color = baseColor.copy(alpha = 0.6f)
|
color = baseColor.copy(alpha = 0.6f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isVerified) {
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Verified,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
tint = Color(0xFF32D74B)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Favorite star with proper filled/outlined states
|
// Favorite star with proper filled/outlined states
|
||||||
|
|||||||
@@ -265,6 +265,37 @@
|
|||||||
<string name="debug_none">none</string>
|
<string name="debug_none">none</string>
|
||||||
<string name="debug_disconnect">disconnect</string>
|
<string name="debug_disconnect">disconnect</string>
|
||||||
<string name="debug_recent_scan_results">recent scan results</string>
|
<string name="debug_recent_scan_results">recent scan results</string>
|
||||||
|
|
||||||
|
<string name="verify_title">verify</string>
|
||||||
|
<string name="verify_my_qr_title">scan to verify me</string>
|
||||||
|
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||||
|
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||||
|
<string name="verify_show_my_qr">show my qr</string>
|
||||||
|
<string name="verify_remove">remove verification</string>
|
||||||
|
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||||
|
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||||
|
<string name="verify_request_camera">enable camera</string>
|
||||||
|
<string name="verify_paste_label">paste verification url</string>
|
||||||
|
<string name="verify_validate">validate</string>
|
||||||
|
<string name="verify_scanned">verification requested</string>
|
||||||
|
<string name="security_verification_title">security verification</string>
|
||||||
|
<string name="fingerprint_their">their fingerprint</string>
|
||||||
|
<string name="fingerprint_yours">your fingerprint</string>
|
||||||
|
<string name="fingerprint_pending">handshake pending</string>
|
||||||
|
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||||
|
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||||
|
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||||
|
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||||
|
<string name="fingerprint_status_failed">handshake failed</string>
|
||||||
|
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||||
|
<string name="fingerprint_verified_label">verified</string>
|
||||||
|
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||||
|
<string name="fingerprint_not_verified_label">not verified</string>
|
||||||
|
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||||
|
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||||
|
<string name="fingerprint_start_handshake">start handshake</string>
|
||||||
|
<string name="fingerprint_copy">copy</string>
|
||||||
|
|
||||||
<string name="debug_connect">connect</string>
|
<string name="debug_connect">connect</string>
|
||||||
<string name="debug_debug_console">debug console</string>
|
<string name="debug_debug_console">debug console</string>
|
||||||
<string name="debug_clear">clear</string>
|
<string name="debug_clear">clear</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user