mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 16:33:27 +00:00
feat(cashu): add-recommendation input + suggest only on typed text
Two changes in the Cashu Wallet Settings flow: 1. My Mint Recommendations now has its own input row. A new OutlinedTextField under the section header lets the user paste a mint URL and tap "+" to publish a kind:38000 recommendation without having to leave Settings, find the mint elsewhere, and thumbs-up it. As the user types, the same cache-backed directory autocomplete that AddCashuWallet uses surfaces matching mints from the kind:10019 / kind:38000 / kind:38172 the cache already holds — tap a suggestion to one-shot recommend (publish + clear the field), useful for chaining several adds. Suggestions are filtered to drop URLs the user has already recommended (de-duped by the lowercased / trailing-slash-stripped mint URL across the user's own kind:38000s) so the same row never appears in both the autocomplete and the list directly below. 2. Autocomplete reacts only to typed text, not to an empty field. The first iteration showed the whole directory the moment the field gained focus — i.e. on the Edit Cashu Wallet mint-URL popup the user saw mint URLs without typing anything, which felt like the form was pre-populating itself. Both call sites (AddCashuWalletScreen + CashuWalletSettingsScreen) now early-return an empty suggestion list when the trimmed input is blank, so the dropdown is purely a reaction to what the user types. https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
This commit is contained in:
+11
-5
@@ -244,14 +244,20 @@ fun AddCashuWalletScreen(
|
||||
// the current input (no point suggesting what they already
|
||||
// typed). Wrapped in `derivedStateOf` so the recompute only
|
||||
// fires when `mintInput` or `mints` change, not on every
|
||||
// recomposition of the surrounding form.
|
||||
// recomposition of the surrounding form. An empty field
|
||||
// shows no suggestions — the autocomplete should react to
|
||||
// typing, not dump every mint we've ever seen unsolicited.
|
||||
val suggestions by remember(mints) {
|
||||
derivedStateOf {
|
||||
val typed = mintInput.trim().trimEnd('/').lowercase()
|
||||
val alreadyAdded = mints.map { it.lowercase().trimEnd('/') }.toSet()
|
||||
LocalCache.mintDirectory
|
||||
.suggest(typed, limit = 6)
|
||||
.filter { it != typed && it !in alreadyAdded }
|
||||
if (typed.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
val alreadyAdded = mints.map { it.lowercase().trimEnd('/') }.toSet()
|
||||
LocalCache.mintDirectory
|
||||
.suggest(typed, limit = 6)
|
||||
.filter { it != typed && it !in alreadyAdded }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (suggestions.isNotEmpty()) {
|
||||
|
||||
+148
@@ -41,12 +41,15 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -60,6 +63,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -92,6 +96,11 @@ fun CashuWalletSettingsScreen(
|
||||
|
||||
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.
|
||||
LaunchedEffect(Unit) { LocalCache.ensureMintDirectoryBackfilled() }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -136,6 +145,65 @@ fun CashuWalletSettingsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// 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 = ""
|
||||
}
|
||||
},
|
||||
)
|
||||
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()) {
|
||||
item { EmptyRecommendationsHint() }
|
||||
} else {
|
||||
@@ -228,6 +296,86 @@ private fun SettingsRow(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddRecommendationRow(
|
||||
input: String,
|
||||
onInputChange: (String) -> Unit,
|
||||
canAdd: Boolean,
|
||||
onAdd: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = input,
|
||||
onValueChange = onInputChange,
|
||||
label = { Text(stringRes(R.string.cashu_settings_add_recommendation)) },
|
||||
placeholder = { Text("https://mint.example.com") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
IconButton(onClick = onAdd, enabled = canAdd) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Add,
|
||||
contentDescription = stringRes(R.string.cashu_settings_add_recommendation),
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint =
|
||||
if (canAdd) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecommendationSuggestionList(
|
||||
suggestions: List<String>,
|
||||
onPick: (String) -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 4.dp)) {
|
||||
suggestions.forEach { url ->
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onPick(url) }
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.AccountBalanceWallet,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = url,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Icon(
|
||||
symbol = MaterialSymbols.ThumbUp,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyRecommendationsHint() {
|
||||
Card(
|
||||
|
||||
@@ -1926,6 +1926,7 @@
|
||||
<string name="cashu_settings_delete_recommendation">Delete recommendation</string>
|
||||
<string name="cashu_settings_delete_confirm_title">Retract recommendation?</string>
|
||||
<string name="cashu_settings_delete_confirm_body">Publish a delete request for your kind:38000 recommendation of %1$s. Relays that honor NIP-09 will drop it.</string>
|
||||
<string name="cashu_settings_add_recommendation">Recommend a mint</string>
|
||||
<string name="cashu_pay_invoice">Pay invoice</string>
|
||||
<string name="cashu_get_quote">Get quote</string>
|
||||
<string name="cashu_getting_quote">Asking mint for a quote…</string>
|
||||
|
||||
Reference in New Issue
Block a user