From 4fdc9babbe01098b5ab01f0e2b8539bff02c6a36 Mon Sep 17 00:00:00 2001
From: callebtc <93376500+callebtc@users.noreply.github.com>
Date: Wed, 29 Jul 2026 19:11:06 +0200
Subject: [PATCH] feat: unify people lists with conversation-row avatar design
- Extract shared PeerAvatar (initial circle + lower-right transport badge)
from the conversation row and use it in the mesh peer list and the
geohash/Nostr people list
- Remove the favorite toggle button from the peer list; favorite state is
now a small star badge on the avatar (filled = we favorited, outline =
they favorited us), so favoriting only happens from the private chat
- Show the unread-count badge on peer rows, matching conversation rows
---
.../bitchat/android/ui/GeohashPeopleList.kt | 41 ++--
.../bitchat/android/ui/MeshPeerListSheet.kt | 222 +++++++-----------
.../java/com/bitchat/android/ui/PeerAvatar.kt | 105 +++++++++
app/src/main/res/values/strings.xml | 2 +
4 files changed, 213 insertions(+), 157 deletions(-)
create mode 100644 app/src/main/java/com/bitchat/android/ui/PeerAvatar.kt
diff --git a/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt b/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt
index d784610a..0c0e583f 100644
--- a/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt
+++ b/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt
@@ -281,6 +281,7 @@ private fun GeohashPersonItem(
onTap: () -> Unit
) {
val palette = LocalBitchatPalette.current
+ val colorScheme = MaterialTheme.colorScheme
val statusIconRes =
if (isTeleported) R.drawable.ic_spec_teleport
@@ -298,25 +299,14 @@ private fun GeohashPersonItem(
Row(
modifier = Modifier
.fillMaxWidth()
- // Exact height, not padding: a row that sizes to its content makes the card change
- // height whenever the list reorders.
- .height(SheetRowHeight)
.clickable(onClick = onTap)
- .padding(horizontal = SheetRowHorizontal),
+ .padding(horizontal = SheetRowHorizontal, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
- Box(
- modifier = Modifier.size(SheetRowLeadingSlot),
- contentAlignment = Alignment.Center
- ) {
- if (hasUnreadDM) {
- Icon(
- imageVector = Icons.Filled.Email,
- contentDescription = stringResource(R.string.cd_unread_message),
- modifier = Modifier.size(22.dp),
- tint = palette.accentOrange
- )
- } else {
+ PeerAvatar(
+ name = baseNameRaw,
+ color = baseColor,
+ badge = {
Icon(
painter = painterResource(statusIconRes),
contentDescription = if (isTeleported) {
@@ -324,13 +314,13 @@ private fun GeohashPersonItem(
} else {
stringResource(R.string.section_on_location)
},
- modifier = Modifier.size(22.dp),
- tint = baseColor
+ modifier = Modifier.size(13.dp),
+ tint = if (isTeleported) palette.accentPurple else colorScheme.primary
)
}
- }
+ )
- Spacer(modifier = Modifier.width(SheetRowLeadingGutter))
+ Spacer(modifier = Modifier.width(12.dp))
Row(
modifier = Modifier.weight(1f),
@@ -365,5 +355,16 @@ private fun GeohashPersonItem(
)
}
}
+
+ if (hasUnreadDM) {
+ Icon(
+ imageVector = Icons.Filled.Email,
+ contentDescription = stringResource(R.string.cd_unread_message),
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .size(18.dp),
+ tint = palette.accentOrange
+ )
+ }
}
}
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..89746a56 100644
--- a/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt
+++ b/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt
@@ -831,18 +831,14 @@ fun PeopleSection(
displayName = displayName,
isDirect = isDirectLive,
isWifiAware = peerID in wifiAwarePeerIDs,
+ isConnected = true,
isSelected = conversationID == selectedPrivatePeer || peerID == selectedPrivatePeer,
isFavorite = isFavorite,
theyFavoritedUs = theyFavoritedUs,
isVerified = isVerified,
- hasUnreadDM = combinedHasUnread,
colorScheme = colorScheme,
viewModel = viewModel,
onItemClick = { onPrivateChatStart(peerID) },
- onToggleFavorite = {
- Log.d("SidebarComponents", "Sidebar toggle favorite: peerID=$peerID, currentFavorite=$isFavorite")
- viewModel.toggleFavorite(peerID)
- },
unreadCount = if (combinedUnreadCount > 0) combinedUnreadCount else if (combinedHasUnread) 1 else 0,
showNostrGlobe = false,
showHashSuffix = showHash
@@ -881,18 +877,14 @@ fun PeopleSection(
peerID = favPeerID,
displayName = dn,
isDirect = false,
+ isConnected = false,
isSelected = conversationID == selectedPrivatePeer || (mappedConnectedPeerID ?: favPeerID) == selectedPrivatePeer,
isFavorite = true,
theyFavoritedUs = fav.theyFavoritedUs,
isVerified = isVerified,
- hasUnreadDM = hasUnread,
colorScheme = colorScheme,
viewModel = viewModel,
onItemClick = { onPrivateChatStart(mappedConnectedPeerID ?: favPeerID) },
- onToggleFavorite = {
- Log.d("SidebarComponents", "Sidebar toggle favorite (offline): peerID=$favPeerID")
- viewModel.toggleFavorite(favPeerID)
- },
unreadCount = if (unreadCount > 0) unreadCount else if (hasUnread) 1 else 0,
showNostrGlobe = (fav.isMutual && fav.peerNostrPublicKey != null),
showHashSuffix = showHash
@@ -1263,72 +1255,42 @@ private fun ConversationRow(
),
verticalAlignment = Alignment.CenterVertically
) {
- Box(
- modifier = Modifier.size(42.dp),
- contentAlignment = Alignment.Center,
- ) {
- Box(
- modifier = Modifier
- .size(38.dp)
- .background(assignedColor.copy(alpha = 0.16f), CircleShape),
- contentAlignment = Alignment.Center
- ) {
- Text(
- text = baseNameRaw
- .trim()
- .firstOrNull()
- ?.uppercase()
- ?: "#",
- style = MaterialTheme.typography.titleMedium.copy(
- fontFamily = BitchatFontFamily,
- fontWeight = FontWeight.SemiBold
- ),
- color = assignedColor
- )
- }
+ PeerAvatar(
+ name = baseNameRaw,
+ color = assignedColor,
+ badge = {
+ when {
+ conversation.isConnected -> Icon(
+ painter = painterResource(
+ conversationTransportIcon(
+ isReachedOverInternet = false,
+ isWifiAware = isWifiAware,
+ isDirect = isDirect
+ )
+ ),
+ contentDescription = connectionDescription,
+ modifier = Modifier.size(13.dp),
+ tint = colorScheme.primary
+ )
- Surface(
- modifier = Modifier
- .size(18.dp)
- .align(Alignment.BottomEnd),
- shape = CircleShape,
- color = colorScheme.surface,
- tonalElevation = 1.dp
- ) {
- Box(contentAlignment = Alignment.Center) {
- when {
- conversation.isConnected -> Icon(
- painter = painterResource(
- conversationTransportIcon(
- isReachedOverInternet = false,
- isWifiAware = isWifiAware,
- isDirect = isDirect
- )
- ),
- contentDescription = connectionDescription,
- modifier = Modifier.size(13.dp),
- tint = colorScheme.primary
- )
+ conversation.transport == DirectMessageTransport.NOSTR -> Icon(
+ painter = painterResource(R.drawable.ic_spec_globe),
+ contentDescription = stringResource(
+ R.string.offline_reachable_via_nostr
+ ),
+ modifier = Modifier.size(13.dp),
+ tint = palette.accentPurple
+ )
- conversation.transport == DirectMessageTransport.NOSTR -> Icon(
- painter = painterResource(R.drawable.ic_spec_globe),
- contentDescription = stringResource(
- R.string.offline_reachable_via_nostr
- ),
- modifier = Modifier.size(13.dp),
- tint = palette.accentPurple
- )
-
- else -> Icon(
- imageVector = Icons.Outlined.Circle,
- contentDescription = stringResource(R.string.offline_not_in_mesh),
- modifier = Modifier.size(11.dp),
- tint = palette.textTertiary
- )
- }
+ else -> Icon(
+ imageVector = Icons.Outlined.Circle,
+ contentDescription = stringResource(R.string.offline_not_in_mesh),
+ modifier = Modifier.size(11.dp),
+ tint = palette.textTertiary
+ )
}
}
- }
+ )
Spacer(modifier = Modifier.width(12.dp))
@@ -1559,15 +1521,14 @@ private fun PeerItem(
displayName: String,
isDirect: Boolean,
isWifiAware: Boolean = false,
+ isConnected: Boolean = true,
isSelected: Boolean,
isFavorite: Boolean,
theyFavoritedUs: Boolean = false,
isVerified: Boolean,
- hasUnreadDM: Boolean,
colorScheme: ColorScheme,
viewModel: ChatViewModel,
onItemClick: () -> Unit,
- onToggleFavorite: () -> Unit,
unreadCount: Int = 0,
showNostrGlobe: Boolean = false,
showHashSuffix: Boolean = true
@@ -1595,57 +1556,47 @@ private fun PeerItem(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onItemClick)
- .padding(horizontal = SheetRowHorizontal, vertical = SheetRowVertical),
+ .padding(horizontal = SheetRowHorizontal, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
- Box(
- modifier = Modifier.size(SheetRowLeadingSlot),
- contentAlignment = Alignment.Center
- ) {
- if (isSelected) {
- Box(
- modifier = Modifier
- .size(SheetRowSelectedDot)
- .background(colorScheme.primary, CircleShape)
- )
- } else if (hasUnreadDM) {
- Icon(
- painter = painterResource(R.drawable.ic_spec_envelope),
- contentDescription = stringResource(R.string.cd_unread_message),
- modifier = Modifier.size(PeerRowIconSize),
- tint = palette.accentOrange
- )
- } else if (showNostrGlobe) {
- Icon(
- painter = painterResource(R.drawable.ic_spec_globe),
- contentDescription = stringResource(R.string.cd_reachable_via_nostr),
- modifier = Modifier.size(PeerRowIconSize),
- tint = palette.accentPurple
- )
- } else if (!isDirect && isFavorite) {
- Icon(
- imageVector = Icons.Outlined.Circle,
- contentDescription = stringResource(R.string.cd_offline_favorite),
- modifier = Modifier.size(PeerRowIconSize),
- tint = palette.textTertiary
- )
- } else {
- Icon(
- painter = painterResource(
- conversationTransportIcon(
- isReachedOverInternet = false,
- isWifiAware = isWifiAware,
- isDirect = isDirect
- )
- ),
- contentDescription = connectionDescription,
- modifier = Modifier.size(PeerRowIconSize),
- tint = colorScheme.onSurfaceVariant
- )
- }
- }
+ PeerAvatar(
+ name = baseNameRaw,
+ color = baseColor,
+ isFavorite = isFavorite,
+ theyFavoritedUs = theyFavoritedUs,
+ badge = {
+ when {
+ isConnected -> Icon(
+ painter = painterResource(
+ conversationTransportIcon(
+ isReachedOverInternet = false,
+ isWifiAware = isWifiAware,
+ isDirect = isDirect
+ )
+ ),
+ contentDescription = connectionDescription,
+ modifier = Modifier.size(13.dp),
+ tint = colorScheme.primary
+ )
- Spacer(modifier = Modifier.width(SheetRowLeadingGutter))
+ showNostrGlobe -> Icon(
+ painter = painterResource(R.drawable.ic_spec_globe),
+ contentDescription = stringResource(R.string.cd_reachable_via_nostr),
+ modifier = Modifier.size(13.dp),
+ tint = palette.accentPurple
+ )
+
+ else -> Icon(
+ imageVector = Icons.Outlined.Circle,
+ contentDescription = stringResource(R.string.cd_offline_favorite),
+ modifier = Modifier.size(11.dp),
+ tint = palette.textTertiary
+ )
+ }
+ }
+ )
+
+ Spacer(modifier = Modifier.width(12.dp))
Row(
modifier = Modifier.weight(1f),
@@ -1682,21 +1633,18 @@ private fun PeerItem(
}
}
- Box(
- modifier = Modifier
- .size(36.dp)
- .clickable(onClick = onToggleFavorite),
- contentAlignment = Alignment.Center
- ) {
- // Three-state star (matches private-chat header): grey outline (no relation),
- // orange outline (they favorited us), filled orange (we favorited them).
- Icon(
- painter = painterResource(
- if (isFavorite) R.drawable.ic_spec_star_filled else R.drawable.ic_spec_star
- ),
- contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
- modifier = Modifier.size(PeerRowIconSize),
- tint = if (isFavorite || theyFavoritedUs) palette.accentOrange else palette.textTertiary
+ UnreadBadge(
+ count = unreadCount,
+ colorScheme = colorScheme,
+ modifier = Modifier.padding(start = 4.dp)
+ )
+
+ if (isSelected) {
+ Box(
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .size(SheetRowSelectedDot)
+ .background(colorScheme.primary, CircleShape)
)
}
}
diff --git a/app/src/main/java/com/bitchat/android/ui/PeerAvatar.kt b/app/src/main/java/com/bitchat/android/ui/PeerAvatar.kt
new file mode 100644
index 00000000..fcdf4146
--- /dev/null
+++ b/app/src/main/java/com/bitchat/android/ui/PeerAvatar.kt
@@ -0,0 +1,105 @@
+package com.bitchat.android.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.bitchat.android.R
+import com.bitchat.android.ui.theme.BitchatFontFamily
+import com.bitchat.android.ui.theme.LocalBitchatPalette
+
+internal val PeerAvatarBadgeSize = 18.dp
+private val PeerAvatarStarSize = 16.dp
+
+@Composable
+internal fun PeerAvatar(
+ name: String,
+ color: Color,
+ modifier: Modifier = Modifier,
+ isFavorite: Boolean = false,
+ theyFavoritedUs: Boolean = false,
+ badge: (@Composable () -> Unit)? = null
+) {
+ val palette = LocalBitchatPalette.current
+ val colorScheme = MaterialTheme.colorScheme
+
+ Box(
+ modifier = modifier.size(42.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .size(38.dp)
+ .background(color.copy(alpha = 0.16f), CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = name.trim().firstOrNull()?.uppercase() ?: "#",
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontFamily = BitchatFontFamily,
+ fontWeight = FontWeight.SemiBold
+ ),
+ color = color
+ )
+ }
+
+ if (badge != null) {
+ Surface(
+ modifier = Modifier
+ .size(PeerAvatarBadgeSize)
+ .align(Alignment.BottomEnd),
+ shape = CircleShape,
+ color = colorScheme.surface,
+ tonalElevation = 1.dp
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ badge()
+ }
+ }
+ }
+
+ if (isFavorite || theyFavoritedUs) {
+ Surface(
+ modifier = Modifier
+ .size(PeerAvatarStarSize)
+ .align(Alignment.TopEnd),
+ shape = CircleShape,
+ color = colorScheme.surface,
+ tonalElevation = 1.dp
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ Icon(
+ painter = painterResource(
+ if (isFavorite) {
+ R.drawable.ic_spec_star_filled
+ } else {
+ R.drawable.ic_spec_star
+ }
+ ),
+ contentDescription = stringResource(
+ if (isFavorite) {
+ R.string.cd_favorite
+ } else {
+ R.string.cd_favorited_you
+ }
+ ),
+ modifier = Modifier.size(10.dp),
+ tint = palette.accentOrange
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index eeb01c75..236ba20c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -69,6 +69,8 @@
Add to favorites
Remove from favorites
+ Favorite
+ Favorited you
Add bookmark