Merge pull request #3365 from vitorpamplona/claude/happy-brown-tlu9nq

Extract AddRecommendationSection and WalletTypeCardContent composables
This commit is contained in:
Vitor Pamplona
2026-06-25 15:23:15 -04:00
committed by GitHub
3 changed files with 135 additions and 112 deletions
@@ -130,41 +130,50 @@ private fun WalletTypeCard(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
) {
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
symbol = icon,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary,
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(modifier = Modifier.width(8.dp))
Icon(
symbol = MaterialSymbols.AutoMirrored.ArrowForward,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(modifier = Modifier.height(4.dp))
WalletTypeCardContent(icon = icon, title = title, description = description)
}
}
@Composable
private fun WalletTypeCardContent(
icon: MaterialSymbol,
title: String,
description: String,
) {
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
symbol = icon,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary,
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(modifier = Modifier.width(8.dp))
Icon(
symbol = MaterialSymbols.AutoMirrored.ArrowForward,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(modifier = Modifier.height(4.dp))
}
@@ -93,7 +93,6 @@ fun CashuMintRecommendationsScreen(
val recommendations by viewModel.ownRecommendations.collectAsState()
var pendingDelete by remember { mutableStateOf<MintRecommendationEvent?>(null) }
var newRecommendationInput by remember { mutableStateOf("") }
// Kick the one-shot directory backfill so the autocomplete is useful
// on first screen open instead of waiting for new relay deliveries.
@@ -126,63 +125,11 @@ fun CashuMintRecommendationsScreen(
) {
item { Spacer(modifier = Modifier.height(8.dp)) }
// Add-recommendation input + autocomplete. Suggestions come
// from LocalCache.mintDirectory (kind:10019 / kind:38000 /
// kind:38172 the cache has seen), filtered to drop URLs the
// user has already recommended so they can't double-publish
// and the same row doesn't show up twice on screen.
item {
val alreadyRecommended =
remember(recommendations) {
recommendations
.flatMap { it.mintUrls() }
.map { it.lowercase().trimEnd('/') }
.toSet()
}
val suggestions by remember(newRecommendationInput, alreadyRecommended) {
derivedStateOf {
val typed = newRecommendationInput.trim().trimEnd('/').lowercase()
// Only react to what the user types — never show
// the full directory on an empty field, which
// would dump every mint we've ever seen as
// unsolicited suggestions.
if (typed.isEmpty()) {
emptyList()
} else {
LocalCache.mintDirectory
.suggest(typed, limit = 6)
.filter { it != typed && it !in alreadyRecommended }
}
}
}
AddRecommendationRow(
input = newRecommendationInput,
onInputChange = { newRecommendationInput = it },
canAdd =
newRecommendationInput.isNotBlank() &&
newRecommendationInput.trim().trimEnd('/').lowercase() !in alreadyRecommended,
onAdd = {
val trimmed = newRecommendationInput.trim().trimEnd('/')
if (trimmed.isNotEmpty()) {
viewModel.recommendMint(trimmed)
newRecommendationInput = ""
}
},
AddRecommendationSection(
recommendations = recommendations,
onRecommendMint = { viewModel.recommendMint(it) },
)
if (suggestions.isNotEmpty()) {
Spacer(modifier = Modifier.height(6.dp))
RecommendationSuggestionList(
suggestions = suggestions,
onPick = { url ->
// One-tap recommend: publish straight from
// the suggestion and clear the field so the
// user can chain multiple adds without
// re-tapping the text input.
viewModel.recommendMint(url)
newRecommendationInput = ""
},
)
}
}
if (recommendations.isEmpty()) {
@@ -227,6 +174,66 @@ fun CashuMintRecommendationsScreen(
}
}
@Composable
private fun AddRecommendationSection(
recommendations: List<MintRecommendationEvent>,
onRecommendMint: (String) -> Unit,
) {
var newRecommendationInput by remember { mutableStateOf("") }
val alreadyRecommended =
remember(recommendations) {
recommendations
.flatMap { it.mintUrls() }
.map { it.lowercase().trimEnd('/') }
.toSet()
}
val suggestions by remember(newRecommendationInput, alreadyRecommended) {
derivedStateOf {
val typed = newRecommendationInput.trim().trimEnd('/').lowercase()
// Only react to what the user types — never show
// the full directory on an empty field, which
// would dump every mint we've ever seen as
// unsolicited suggestions.
if (typed.isEmpty()) {
emptyList()
} else {
LocalCache.mintDirectory
.suggest(typed, limit = 6)
.filter { it != typed && it !in alreadyRecommended }
}
}
}
AddRecommendationRow(
input = newRecommendationInput,
onInputChange = { newRecommendationInput = it },
canAdd =
newRecommendationInput.isNotBlank() &&
newRecommendationInput.trim().trimEnd('/').lowercase() !in alreadyRecommended,
onAdd = {
val trimmed = newRecommendationInput.trim().trimEnd('/')
if (trimmed.isNotEmpty()) {
onRecommendMint(trimmed)
newRecommendationInput = ""
}
},
)
if (suggestions.isNotEmpty()) {
Spacer(modifier = Modifier.height(6.dp))
RecommendationSuggestionList(
suggestions = suggestions,
onPick = { url ->
// One-tap recommend: publish straight from
// the suggestion and clear the field so the
// user can chain multiple adds without
// re-tapping the text input.
onRecommendMint(url)
newRecommendationInput = ""
},
)
}
}
@Composable
private fun AddRecommendationRow(
input: String,
@@ -172,30 +172,37 @@ private fun HeaderRow(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
BitcoinChip(orange)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = "Bitcoin",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
Spacer(modifier = Modifier.width(8.dp))
PublicChip()
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Onchain · Taproot",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
HeaderRowTitle(modifier = Modifier.weight(1f), orange = orange)
BalanceBlock(state = balanceState, sats = balanceSats, orange = orange)
}
}
@Composable
private fun HeaderRowTitle(
modifier: Modifier = Modifier,
orange: Color,
) {
Column(modifier = modifier) {
Row(verticalAlignment = Alignment.CenterVertically) {
BitcoinChip(orange)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = "Bitcoin",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
Spacer(modifier = Modifier.width(8.dp))
PublicChip()
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Onchain · Taproot",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@Composable
private fun PublicChip() {
var showDialog by remember { mutableStateOf(false) }