mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-29 07:16:08 +00:00
lists
This commit is contained in:
parent
521784b359
commit
3dbe9c1c52
@ -68,6 +68,15 @@ internal val AboutHorizontalPadding = 20.dp
|
||||
/** Card corner radius for grouped rows. */
|
||||
internal val AboutCardShape = RoundedCornerShape(16.dp)
|
||||
|
||||
/** Leading icon column in settings-style sheet rows. */
|
||||
internal val SheetRowLeadingSlot = 22.dp
|
||||
internal val SheetRowLeadingGutter = 16.dp
|
||||
internal val SheetRowHorizontal = 16.dp
|
||||
internal val SheetRowVertical = 13.dp
|
||||
internal val SheetRowDividerInset = SheetRowHorizontal + SheetRowLeadingSlot + SheetRowLeadingGutter
|
||||
/** Selection indicator sized for [SheetRowLeadingSlot]. */
|
||||
internal val SheetRowSelectedDot = 12.dp
|
||||
|
||||
/**
|
||||
* Two top-level views of the sheet: what the app is and how to drive it, versus the knobs.
|
||||
*/
|
||||
@ -99,6 +108,66 @@ internal fun AboutSectionLabel(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon + title on one line, optional short subtitle beneath. Used by location / network sheets.
|
||||
*/
|
||||
@Composable
|
||||
internal fun SheetIconSectionHeader(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = colorScheme.primary,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 17.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = colorScheme.primary
|
||||
)
|
||||
}
|
||||
if (!subtitle.isNullOrBlank()) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 17.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = palette.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Inset divider used inside grouped sheet cards (aligns with text column after the leading slot). */
|
||||
@Composable
|
||||
internal fun SheetCardDivider() {
|
||||
val palette = LocalBitchatPalette.current
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(start = SheetRowDividerInset),
|
||||
thickness = 1.dp,
|
||||
color = palette.outlineVariant
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Centered app identity block: logo, wordmark, tagline, version.
|
||||
*/
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
package com.bitchat.android.ui
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.filled.Email
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.outlined.Explore
|
||||
import androidx.compose.material.icons.outlined.LocationOn
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@ -17,25 +20,20 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||
import java.util.*
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* GeohashPeopleList - iOS-compatible component for displaying geohash participants
|
||||
* Shows peers discovered through Nostr ephemeral events instead of Bluetooth peers
|
||||
* Geohash people list — card groups matching location / settings sheet rows.
|
||||
*/
|
||||
|
||||
/**
|
||||
* GeoPerson data class - matches iOS GeoPerson structure exactly
|
||||
*/
|
||||
data class GeoPerson(
|
||||
val id: String, // pubkey hex (lowercased) - matches iOS
|
||||
val displayName: String, // nickname with #suffix - matches iOS
|
||||
val displayName: String, // nickname with #suffix - matches iOS
|
||||
val lastSeen: Date // activity timestamp - matches iOS
|
||||
)
|
||||
|
||||
@ -46,32 +44,41 @@ fun GeohashPeopleList(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
// Observe geohash people from ChatViewModel
|
||||
|
||||
val geohashPeople by viewModel.geohashPeople.collectAsStateWithLifecycle()
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||
val isTeleported by viewModel.isTeleported.collectAsStateWithLifecycle()
|
||||
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||
val unreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
|
||||
|
||||
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
Column(modifier = modifier) {
|
||||
if (geohashPeople.isEmpty()) {
|
||||
SheetSectionLabel(text = stringResource(R.string.section_on_location))
|
||||
// Empty state - matches iOS "nobody around..."
|
||||
Text(
|
||||
text = stringResource(R.string.nobody_around),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = palette.textTertiary,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = SheetHorizontalPadding + 14.dp,
|
||||
vertical = 12.dp
|
||||
)
|
||||
SheetIconSectionHeader(
|
||||
icon = Icons.Outlined.LocationOn,
|
||||
title = stringResource(R.string.section_on_location)
|
||||
)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding)
|
||||
.padding(top = 10.dp),
|
||||
color = palette.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.nobody_around),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = palette.textTertiary,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = SheetRowHorizontal,
|
||||
vertical = SheetRowVertical
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Get current geohash identity for "me" detection
|
||||
val myHex = remember(selectedLocationChannel) {
|
||||
when (val channel = selectedLocationChannel) {
|
||||
is com.bitchat.android.geohash.ChannelID.Location -> {
|
||||
@ -89,30 +96,26 @@ fun GeohashPeopleList(
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
// Sort people: me first, then by lastSeen (matches iOS exactly)
|
||||
|
||||
val orderedPeople = remember(geohashPeople, myHex) {
|
||||
geohashPeople.sortedWith { a, b ->
|
||||
when {
|
||||
myHex != null && a.id == myHex && b.id != myHex -> -1
|
||||
myHex != null && b.id == myHex && a.id != myHex -> 1
|
||||
else -> b.lastSeen.compareTo(a.lastSeen) // Most recent first
|
||||
else -> b.lastSeen.compareTo(a.lastSeen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute base name collisions to decide whether to show hash suffix
|
||||
val baseNameCounts = remember(geohashPeople) {
|
||||
val counts = mutableMapOf<String, Int>()
|
||||
geohashPeople.forEach { person ->
|
||||
val (b, _) = com.bitchat.android.ui.splitSuffix(person.displayName)
|
||||
val (b, _) = splitSuffix(person.displayName)
|
||||
counts[b] = (counts[b] ?: 0) + 1
|
||||
}
|
||||
counts
|
||||
}
|
||||
|
||||
// Split by presence: people physically in the geohash versus people who teleported
|
||||
// in. Mixing them hid the fact that a "nearby" channel can contain remote users.
|
||||
|
||||
fun personTeleported(person: GeoPerson): Boolean = if (person.id == myHex) {
|
||||
isTeleported
|
||||
} else {
|
||||
@ -132,11 +135,9 @@ fun GeohashPeopleList(
|
||||
nickname = nickname,
|
||||
colorScheme = colorScheme,
|
||||
viewModel = viewModel,
|
||||
showHashSuffix = (baseNameCounts[com.bitchat.android.ui.splitSuffix(person.displayName).first] ?: 0) > 1,
|
||||
showHashSuffix = (baseNameCounts[splitSuffix(person.displayName).first] ?: 0) > 1,
|
||||
onTap = {
|
||||
if (person.id != myHex) {
|
||||
// TODO: Re-enable when NIP-17 geohash DM issues are fixed
|
||||
// Start geohash DM (iOS-compatible)
|
||||
viewModel.startGeohashDM(person.id)
|
||||
onTapPerson()
|
||||
}
|
||||
@ -145,13 +146,48 @@ fun GeohashPeopleList(
|
||||
}
|
||||
|
||||
if (localPeople.isNotEmpty()) {
|
||||
SheetSectionLabel(text = stringResource(R.string.section_on_location))
|
||||
localPeople.forEach { personRow(it) }
|
||||
SheetIconSectionHeader(
|
||||
icon = Icons.Outlined.LocationOn,
|
||||
title = stringResource(R.string.section_on_location)
|
||||
)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding)
|
||||
.padding(top = 10.dp),
|
||||
color = palette.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Column {
|
||||
localPeople.forEachIndexed { index, person ->
|
||||
if (index > 0) SheetCardDivider()
|
||||
personRow(person)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (teleportedPeople.isNotEmpty()) {
|
||||
SheetSectionLabel(text = stringResource(R.string.section_teleported_in))
|
||||
teleportedPeople.forEach { personRow(it) }
|
||||
SheetIconSectionHeader(
|
||||
icon = Icons.Outlined.Explore,
|
||||
title = stringResource(R.string.section_teleported_in),
|
||||
modifier = Modifier.padding(top = if (localPeople.isNotEmpty()) 20.dp else 0.dp)
|
||||
)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding)
|
||||
.padding(top = 10.dp),
|
||||
color = palette.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Column {
|
||||
teleportedPeople.forEachIndexed { index, person ->
|
||||
if (index > 0) SheetCardDivider()
|
||||
personRow(person)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -172,69 +208,57 @@ private fun GeohashPersonItem(
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
Surface(
|
||||
onClick = onTap,
|
||||
color = palette.surface,
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SheetHorizontalPadding, vertical = 3.dp)
|
||||
) {
|
||||
val (iconName, iconColor) = when {
|
||||
isMe && isMyTeleported -> "face.dashed" to palette.accentOrange
|
||||
isTeleported -> "face.dashed" to palette.textSecondary
|
||||
isMe -> "face.smiling" to palette.accentOrange
|
||||
else -> "face.smiling" to palette.textSecondary
|
||||
}
|
||||
val statusIcon = when (iconName) {
|
||||
"face.dashed" -> Icons.Outlined.Explore
|
||||
else -> Icons.Outlined.LocationOn
|
||||
}
|
||||
|
||||
val (baseNameRaw, suffixRaw) = splitSuffix(person.displayName)
|
||||
val baseName = truncateNickname(baseNameRaw)
|
||||
val suffix = if (showHashSuffix) suffixRaw else ""
|
||||
val assignedColor = viewModel.colorForNostrPubkey(person.id, palette.isDark)
|
||||
val baseColor = if (isMe) palette.accentOrange else assignedColor
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 44.dp)
|
||||
.padding(horizontal = 14.dp),
|
||||
.clickable(onClick = onTap)
|
||||
.padding(horizontal = SheetRowHorizontal, vertical = SheetRowVertical),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Icon logic matching iOS exactly
|
||||
if (hasUnreadDM) {
|
||||
// Unread DM indicator (orange envelope)
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Email,
|
||||
contentDescription = stringResource(R.string.cd_unread_message),
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = palette.accentOrange
|
||||
)
|
||||
} else {
|
||||
// Face icon with teleportation state
|
||||
val (iconName, iconColor) = when {
|
||||
isMe && isMyTeleported -> "face.dashed" to palette.accentOrange
|
||||
isTeleported -> "face.dashed" to palette.textSecondary
|
||||
isMe -> "face.smiling" to palette.accentOrange
|
||||
else -> "face.smiling" to palette.textSecondary
|
||||
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 {
|
||||
Icon(
|
||||
imageVector = statusIcon,
|
||||
contentDescription = if (isTeleported || isMyTeleported) "Teleported user" else "User",
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint = iconColor.copy(alpha = if (iconName == "face.dashed") 0.6f else 1.0f)
|
||||
)
|
||||
}
|
||||
|
||||
// Teleported users get the pin (they chose this location); locals get the person.
|
||||
val icon = when (iconName) {
|
||||
"face.dashed" -> Icons.Outlined.Explore
|
||||
else -> Icons.Outlined.LocationOn
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = if (isTeleported || isMyTeleported) "Teleported user" else "User",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = iconColor.copy(alpha = if (iconName == "face.dashed") 0.6f else 1.0f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
// Display name with suffix handling
|
||||
val (baseNameRaw, suffixRaw) = com.bitchat.android.ui.splitSuffix(person.displayName)
|
||||
val baseName = truncateNickname(baseNameRaw)
|
||||
val suffix = if (showHashSuffix) suffixRaw else ""
|
||||
|
||||
// Get consistent peer color (matches iOS color assignment exactly)
|
||||
val assignedColor = viewModel.colorForNostrPubkey(person.id, palette.isDark)
|
||||
val baseColor = if (isMe) palette.accentOrange else assignedColor
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.width(SheetRowLeadingGutter))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Base name with peer-specific color
|
||||
Text(
|
||||
text = baseName,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
@ -245,7 +269,6 @@ private fun GeohashPersonItem(
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
// Suffix (collision-resistant #abcd) in lighter shade
|
||||
if (suffix.isNotEmpty()) {
|
||||
Text(
|
||||
text = suffix,
|
||||
@ -256,7 +279,6 @@ private fun GeohashPersonItem(
|
||||
)
|
||||
}
|
||||
|
||||
// "You" indicator for current user
|
||||
if (isMe) {
|
||||
Text(
|
||||
text = stringResource(R.string.you_suffix),
|
||||
@ -267,7 +289,4 @@ private fun GeohashPersonItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -60,13 +60,13 @@ import kotlinx.coroutines.launch
|
||||
* Leading column width matching settings rows: 22.dp glyph + 16.dp gutter before title text.
|
||||
* Selection dots and row icons sit in this column so every option lines up with About settings.
|
||||
*/
|
||||
private val ChannelLeadingSlot = 22.dp
|
||||
private val ChannelLeadingGutter = 16.dp
|
||||
private val ChannelRowHorizontal = 16.dp
|
||||
private val ChannelRowVertical = 13.dp
|
||||
private val ChannelDividerInset = ChannelRowHorizontal + ChannelLeadingSlot + ChannelLeadingGutter
|
||||
private val ChannelLeadingSlot = SheetRowLeadingSlot
|
||||
private val ChannelLeadingGutter = SheetRowLeadingGutter
|
||||
private val ChannelRowHorizontal = SheetRowHorizontal
|
||||
private val ChannelRowVertical = SheetRowVertical
|
||||
private val ChannelDividerInset = SheetRowDividerInset
|
||||
/** 2× the previous 6.dp selected indicator; sits centered in [ChannelLeadingSlot]. */
|
||||
private val ChannelSelectedDot = 12.dp
|
||||
private val ChannelSelectedDot = SheetRowSelectedDot
|
||||
|
||||
/**
|
||||
* Location Channels sheet: grouped card rows matching About → Settings.
|
||||
@ -187,7 +187,7 @@ fun LocationChannelsSheet(
|
||||
// Mesh section: icon + title header, offline subtitle, then selection card
|
||||
item(key = "mesh_card") {
|
||||
Column {
|
||||
ChannelSectionHeader(
|
||||
SheetIconSectionHeader(
|
||||
icon = Icons.Filled.Hub,
|
||||
title = stringResource(R.string.mesh_title),
|
||||
subtitle = stringResource(R.string.mesh_section_subtitle),
|
||||
@ -223,7 +223,7 @@ fun LocationChannelsSheet(
|
||||
// Location channels: globe + title, geohash subtitle, nearby levels + teleport
|
||||
item(key = "channels_card") {
|
||||
Column {
|
||||
ChannelSectionHeader(
|
||||
SheetIconSectionHeader(
|
||||
icon = Icons.Outlined.Public,
|
||||
title = stringResource(R.string.location_channels_heading),
|
||||
subtitle = stringResource(R.string.location_channels_desc),
|
||||
@ -241,7 +241,7 @@ fun LocationChannelsSheet(
|
||||
if (locationServicesEnabled) {
|
||||
if (nearbyChannels.isNotEmpty()) {
|
||||
nearbyChannels.forEachIndexed { index, channel ->
|
||||
if (index > 0) ChannelCardDivider()
|
||||
if (index > 0) SheetCardDivider()
|
||||
val coverage = coverageString(channel.geohash.length)
|
||||
val nameBase = locationNames[channel.level]
|
||||
val namePart = nameBase?.let { formattedNamePrefix(channel.level) + it }
|
||||
@ -269,10 +269,10 @@ fun LocationChannelsSheet(
|
||||
}
|
||||
)
|
||||
}
|
||||
ChannelCardDivider()
|
||||
SheetCardDivider()
|
||||
} else if (showNearbyLoading) {
|
||||
ChannelLoadingRow()
|
||||
ChannelCardDivider()
|
||||
SheetCardDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,7 +352,7 @@ fun LocationChannelsSheet(
|
||||
) {
|
||||
Column {
|
||||
bookmarks.forEachIndexed { index, gh ->
|
||||
if (index > 0) ChannelCardDivider()
|
||||
if (index > 0) SheetCardDivider()
|
||||
val level = levelForLength(gh.length)
|
||||
val channel = GeohashChannel(level = level, geohash = gh)
|
||||
val coverage = coverageString(gh.length)
|
||||
@ -498,63 +498,6 @@ fun LocationChannelsSheet(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelCardDivider() {
|
||||
val palette = LocalBitchatPalette.current
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(start = ChannelDividerInset),
|
||||
thickness = 1.dp,
|
||||
color = palette.outlineVariant
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Section title row: icon + name on one line, optional short subtitle beneath.
|
||||
*/
|
||||
@Composable
|
||||
private fun ChannelSectionHeader(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = colorScheme.primary,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 17.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = colorScheme.primary
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = subtitle,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 17.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = palette.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Single channel option — settings-row geometry: 22.dp leading slot, title + subtitle, trailing.
|
||||
* Selected state is a 12.dp green dot centered in the leading slot (icon-sized footprint).
|
||||
|
||||
@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
@ -109,89 +110,83 @@ fun MeshPeerListSheet(
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(top = 64.dp, bottom = 20.dp)
|
||||
contentPadding = PaddingValues(top = 72.dp, bottom = 32.dp)
|
||||
) {
|
||||
// Header: badge + live participant count
|
||||
item(key = "people_header") {
|
||||
val peopleCount = when (selectedLocationChannel) {
|
||||
is ChannelID.Location -> geohashPeopleCount
|
||||
else -> connectedPeers.count { it != viewModel.myPeerID }
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SheetHorizontalPadding + 14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
SheetHeaderBadge(icon = Icons.Filled.Person)
|
||||
AnimatedCountLabel(
|
||||
count = peopleCount,
|
||||
text = stringResource(R.string.people_count_title, peopleCount),
|
||||
fontSize = 20.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = colorScheme.primary
|
||||
)
|
||||
}
|
||||
val peopleCount = when (selectedLocationChannel) {
|
||||
is ChannelID.Location -> geohashPeopleCount
|
||||
else -> connectedPeers.count { it != viewModel.myPeerID }
|
||||
}
|
||||
|
||||
// Channels section
|
||||
if (joinedChannels.isNotEmpty()) {
|
||||
item(key = "channels_header") {
|
||||
SheetSectionLabel(text = stringResource(R.string.channels))
|
||||
}
|
||||
|
||||
items(
|
||||
items = joinedChannels.toList(),
|
||||
key = { "channel_$it" }
|
||||
) { channel ->
|
||||
val isSelected = channel == currentChannel
|
||||
val unreadCount = unreadChannelMessages[channel] ?: 0
|
||||
|
||||
ChannelRow(
|
||||
channel = channel,
|
||||
isSelected = isSelected,
|
||||
unreadCount = unreadCount,
|
||||
colorScheme = colorScheme,
|
||||
onChannelClick = {
|
||||
// Check if this is a DM channel (starts with @)
|
||||
if (channel.startsWith("@")) {
|
||||
// Extract peer name and find the peer ID
|
||||
val peerName = channel.removePrefix("@")
|
||||
val peerID =
|
||||
peerNicknames.entries.firstOrNull { it.value == peerName }?.key
|
||||
if (peerID != null) {
|
||||
viewModel.showPrivateChatSheet(peerID)
|
||||
onDismiss()
|
||||
item(key = "channels_section") {
|
||||
Column {
|
||||
SheetIconSectionHeader(
|
||||
icon = Icons.Filled.Tag,
|
||||
title = stringResource(R.string.channels),
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding)
|
||||
.padding(top = 10.dp),
|
||||
color = LocalBitchatPalette.current.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Column {
|
||||
joinedChannels.toList().forEachIndexed { index, channel ->
|
||||
if (index > 0) SheetCardDivider()
|
||||
val isSelected = channel == currentChannel
|
||||
val unreadCount = unreadChannelMessages[channel] ?: 0
|
||||
ChannelRow(
|
||||
channel = channel,
|
||||
isSelected = isSelected,
|
||||
unreadCount = unreadCount,
|
||||
colorScheme = colorScheme,
|
||||
onChannelClick = {
|
||||
if (channel.startsWith("@")) {
|
||||
val peerName = channel.removePrefix("@")
|
||||
val peerID =
|
||||
peerNicknames.entries.firstOrNull { it.value == peerName }?.key
|
||||
if (peerID != null) {
|
||||
viewModel.showPrivateChatSheet(peerID)
|
||||
onDismiss()
|
||||
}
|
||||
} else {
|
||||
viewModel.switchToChannel(channel)
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
onLeaveChannel = {
|
||||
viewModel.leaveChannel(channel)
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Regular channel switch
|
||||
viewModel.switchToChannel(channel)
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
onLeaveChannel = {
|
||||
viewModel.leaveChannel(channel)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// People section - switch between mesh and geohash lists (iOS-compatible)
|
||||
// People / geohash participants
|
||||
item(key = "people_section") {
|
||||
when (selectedLocationChannel) {
|
||||
is ChannelID.Location -> {
|
||||
// Show geohash people list when in location channel
|
||||
GeohashPeopleList(
|
||||
viewModel = viewModel,
|
||||
onTapPerson = onDismiss
|
||||
onTapPerson = onDismiss,
|
||||
modifier = Modifier.padding(
|
||||
top = if (joinedChannels.isNotEmpty()) 20.dp else 8.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Show mesh peer list when in mesh channel (default)
|
||||
PeopleSection(
|
||||
modifier = Modifier.padding(top = if (joinedChannels.isNotEmpty()) 16.dp else 0.dp),
|
||||
modifier = Modifier.padding(
|
||||
top = if (joinedChannels.isNotEmpty()) 20.dp else 8.dp
|
||||
),
|
||||
connectedPeers = connectedPeers,
|
||||
peerNicknames = peerNicknames,
|
||||
peerRSSI = peerRSSI,
|
||||
@ -199,6 +194,7 @@ fun MeshPeerListSheet(
|
||||
colorScheme = colorScheme,
|
||||
selectedPrivatePeer = selectedPrivatePeer,
|
||||
wifiAwarePeerIDs = wifiAwarePeerIDs,
|
||||
peopleCount = peopleCount,
|
||||
viewModel = viewModel,
|
||||
onPrivateChatStart = { peerID ->
|
||||
viewModel.showPrivateChatSheet(peerID)
|
||||
@ -239,8 +235,8 @@ fun MeshPeerListSheet(
|
||||
}
|
||||
}
|
||||
|
||||
/** Icon size for rows inside the sheets: one step down from the top bar. */
|
||||
private val PeerRowIconSize = 18.dp
|
||||
/** Icon size for trailing actions on peer rows (matches settings glyph scale). */
|
||||
private val PeerRowIconSize = 22.dp
|
||||
|
||||
@Composable
|
||||
private fun ChannelRow(
|
||||
@ -253,54 +249,50 @@ private fun ChannelRow(
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
Surface(
|
||||
onClick = onChannelClick,
|
||||
color = if (isSelected) palette.surfaceVariant else palette.surface,
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SheetHorizontalPadding, vertical = 3.dp)
|
||||
.clickable(onClick = onChannelClick)
|
||||
.padding(horizontal = SheetRowHorizontal, vertical = SheetRowVertical),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 44.dp)
|
||||
.padding(start = 14.dp, end = 6.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
Box(
|
||||
modifier = Modifier.size(SheetRowLeadingSlot),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Unread badge
|
||||
if (unreadCount > 0) {
|
||||
UnreadBadge(
|
||||
count = unreadCount,
|
||||
colorScheme = colorScheme
|
||||
)
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(SheetRowSelectedDot)
|
||||
.background(palette.accentGreen, CircleShape)
|
||||
)
|
||||
} else if (unreadCount > 0) {
|
||||
UnreadBadge(count = unreadCount, colorScheme = colorScheme)
|
||||
} else {
|
||||
Text(
|
||||
text = channel,
|
||||
text = "#",
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 14.sp,
|
||||
color = if (isSelected) colorScheme.primary else palette.textPrimary,
|
||||
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Leave channel button
|
||||
CloseButton(
|
||||
onClick = onLeaveChannel,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = palette.textTertiary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(SheetRowLeadingGutter))
|
||||
|
||||
Text(
|
||||
text = channel,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 14.sp,
|
||||
color = if (isSelected) colorScheme.primary else palette.textPrimary,
|
||||
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
CloseButton(onClick = onLeaveChannel)
|
||||
}
|
||||
}
|
||||
|
||||
@ -316,6 +308,7 @@ fun PeopleSection(
|
||||
colorScheme: ColorScheme,
|
||||
selectedPrivatePeer: String?,
|
||||
wifiAwarePeerIDs: Set<String> = emptySet(),
|
||||
peopleCount: Int = 0,
|
||||
viewModel: ChatViewModel,
|
||||
onPrivateChatStart: (String) -> Unit
|
||||
) {
|
||||
@ -327,19 +320,31 @@ fun PeopleSection(
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
Column(modifier = modifier) {
|
||||
SheetSectionLabel(text = stringResource(R.string.people))
|
||||
SheetIconSectionHeader(
|
||||
icon = Icons.Filled.Person,
|
||||
title = stringResource(R.string.people_count_title, peopleCount)
|
||||
)
|
||||
|
||||
if (connectedPeers.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.no_one_connected),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = palette.textTertiary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SheetHorizontalPadding + 14.dp, vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AboutHorizontalPadding)
|
||||
.padding(top = 10.dp),
|
||||
color = palette.surface,
|
||||
shape = AboutCardShape
|
||||
) {
|
||||
Column {
|
||||
if (connectedPeers.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.no_one_connected),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = palette.textTertiary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SheetRowHorizontal, vertical = SheetRowVertical)
|
||||
)
|
||||
}
|
||||
|
||||
// Observe reactive state for favorites and fingerprints
|
||||
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
|
||||
@ -443,7 +448,11 @@ fun PeopleSection(
|
||||
if (b != "You") baseNameCounts[b] = (baseNameCounts[b] ?: 0) + 1
|
||||
}
|
||||
|
||||
var peerIndex = 0
|
||||
|
||||
sortedPeers.forEach { peerID ->
|
||||
if (peerIndex > 0) SheetCardDivider()
|
||||
peerIndex++
|
||||
val conversationID = ContactDirectory.canonicalConversationId(peerID)
|
||||
val isFavorite = peerFavoriteStates[peerID] ?: false
|
||||
val isVerified = peerVerifiedStates[peerID] ?: false
|
||||
@ -492,6 +501,9 @@ fun PeopleSection(
|
||||
val favPeerID = ContactIdentityResolver.noiseKeyHex(fav.peerNoisePublicKey)
|
||||
if (isFavoriteMappedToConnected(fav)) return@forEach
|
||||
|
||||
if (peerIndex > 0) SheetCardDivider()
|
||||
peerIndex++
|
||||
|
||||
val nostrConvKey: String? = try {
|
||||
FavoritesPersistenceService.shared.findNostrPubkey(fav.peerNoisePublicKey)
|
||||
?.let { ContactIdentityResolver.nostrAliasForPubkey(it) }
|
||||
@ -535,6 +547,8 @@ fun PeopleSection(
|
||||
showHashSuffix = showHash
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -568,131 +582,112 @@ private fun PeerItem(
|
||||
val assignedColor = viewModel.colorForMeshPeer(peerID, palette.isDark)
|
||||
val baseColor = if (isMe) palette.accentOrange else assignedColor
|
||||
|
||||
Surface(
|
||||
onClick = onItemClick,
|
||||
color = if (isSelected) palette.surfaceVariant else palette.surface,
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SheetHorizontalPadding, vertical = 3.dp)
|
||||
.clickable(onClick = onItemClick)
|
||||
.padding(horizontal = SheetRowHorizontal, vertical = SheetRowVertical),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 44.dp)
|
||||
.padding(start = 14.dp, end = 6.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
Box(
|
||||
modifier = Modifier.size(SheetRowLeadingSlot),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Connection/status indicator
|
||||
if (hasUnreadDM) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Email,
|
||||
contentDescription = stringResource(R.string.cd_unread_message),
|
||||
modifier = Modifier.size(PeerRowIconSize),
|
||||
tint = palette.accentOrange
|
||||
)
|
||||
} else if (showNostrGlobe) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Public,
|
||||
contentDescription = stringResource(R.string.cd_reachable_via_nostr),
|
||||
modifier = Modifier.size(PeerRowIconSize),
|
||||
tint = palette.accentPurple
|
||||
)
|
||||
} else if (!isDirect && isFavorite) {
|
||||
// Offline favourite: routed glyph, dimmed to read as unavailable.
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Circle,
|
||||
contentDescription = stringResource(R.string.cd_offline_favorite),
|
||||
modifier = Modifier.size(PeerRowIconSize),
|
||||
tint = palette.textTertiary
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = when {
|
||||
isWifiAware -> Icons.Filled.Wifi
|
||||
isDirect -> Icons.Outlined.Bluetooth
|
||||
else -> Icons.Filled.Route
|
||||
},
|
||||
contentDescription = when {
|
||||
isWifiAware -> "Direct Wi-Fi Aware"
|
||||
isDirect -> "Direct Bluetooth"
|
||||
else -> "Routed"
|
||||
},
|
||||
modifier = Modifier.size(PeerRowIconSize),
|
||||
tint = palette.textSecondary
|
||||
)
|
||||
}
|
||||
|
||||
// Display name with iOS-style color and hashtag suffix support
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// Base name with peer-specific color
|
||||
Text(
|
||||
text = baseName,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = if (isMe) FontWeight.Bold else FontWeight.Medium,
|
||||
color = baseColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
// Hashtag suffix in lighter shade (iOS-style)
|
||||
if (suffix.isNotEmpty()) {
|
||||
Text(
|
||||
text = suffix,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = if (isMe) FontWeight.Bold else FontWeight.Medium,
|
||||
color = baseColor.copy(alpha = SUFFIX_ALPHA)
|
||||
)
|
||||
}
|
||||
|
||||
if (isWifiAware && hasUnreadDM) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Wifi,
|
||||
contentDescription = "Direct Wi-Fi Aware",
|
||||
modifier = Modifier.size(PeerRowIconSize),
|
||||
tint = palette.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isVerified) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
if (isSelected) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(SheetRowSelectedDot)
|
||||
.background(palette.accentGreen, CircleShape)
|
||||
)
|
||||
} else if (hasUnreadDM) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Verified,
|
||||
contentDescription = stringResource(R.string.verify_title),
|
||||
imageVector = Icons.Filled.Email,
|
||||
contentDescription = stringResource(R.string.cd_unread_message),
|
||||
modifier = Modifier.size(PeerRowIconSize),
|
||||
tint = palette.accentGreen
|
||||
tint = palette.accentOrange
|
||||
)
|
||||
} else if (showNostrGlobe) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Public,
|
||||
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(
|
||||
imageVector = when {
|
||||
isWifiAware -> Icons.Filled.Wifi
|
||||
isDirect -> Icons.Outlined.Bluetooth
|
||||
else -> Icons.Filled.Route
|
||||
},
|
||||
contentDescription = when {
|
||||
isWifiAware -> "Direct Wi-Fi Aware"
|
||||
isDirect -> "Direct Bluetooth"
|
||||
else -> "Routed"
|
||||
},
|
||||
modifier = Modifier.size(PeerRowIconSize),
|
||||
tint = palette.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(SheetRowLeadingGutter))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = baseName,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = if (isMe) FontWeight.Bold else FontWeight.Medium,
|
||||
color = baseColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
if (suffix.isNotEmpty()) {
|
||||
Text(
|
||||
text = suffix,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = if (isMe) FontWeight.Bold else FontWeight.Medium,
|
||||
color = baseColor.copy(alpha = SUFFIX_ALPHA)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Favorite star with proper filled/outlined states
|
||||
IconButton(
|
||||
onClick = onToggleFavorite,
|
||||
modifier = Modifier.size(44.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
|
||||
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
|
||||
modifier = Modifier.size(PeerRowIconSize),
|
||||
tint = if (isFavorite) palette.accentOrange else palette.textTertiary
|
||||
)
|
||||
}
|
||||
if (isVerified) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Verified,
|
||||
contentDescription = stringResource(R.string.verify_title),
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = palette.accentGreen
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clickable(onClick = onToggleFavorite),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
|
||||
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
|
||||
modifier = Modifier.size(PeerRowIconSize),
|
||||
tint = if (isFavorite) palette.accentOrange else palette.textTertiary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user