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