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
This commit is contained in:
callebtc 2026-07-29 19:11:06 +02:00
parent bf5bd8c417
commit 4fdc9babbe
4 changed files with 213 additions and 157 deletions

View File

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

View File

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

View File

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

View File

@ -69,6 +69,8 @@
<!-- Favorites accessibility -->
<string name="cd_add_favorite">Add to favorites</string>
<string name="cd_remove_favorite">Remove from favorites</string>
<string name="cd_favorite">Favorite</string>
<string name="cd_favorited_you">Favorited you</string>
<string name="cd_add_bookmark">Add bookmark</string>
<!-- Chat header &amp; accessibility -->