From 233d0bf08907bbac56c650763631b9b592d1265e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Mar 2026 02:09:28 +0000 Subject: [PATCH] feat: improve Zap Settings UI with sections, chips, and animations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Organize zap settings into three clear sections with primary-colored headers: Quick Zap Amounts, Zap Privacy, and Nostr Wallet Connect - Replace plain Buttons with InputChip for amount selection — each chip shows a lightning bolt, the amount, and a close icon to remove it - Add animateContentSize() to the chips FlowRow so adding/removing amounts animates smoothly - Add descriptive sub-text under each section header explaining what the control does - NWC section: show an animated "Connected / Not Connected" status badge (green CheckCircle vs grey RadioButtonUnchecked) that cross-fades via AnimatedContent when the connection state changes - NWC section: add animateColorAsState for the status color transition - Replace the bare icon-only Add button with an OutlinedButton labelled "Connect Wallet" for better discoverability - Move the manual pubkey/relay/secret fields into a collapsible "Advanced" row animated with AnimatedVisibility (expandVertically + fadeIn/fadeOut). The section auto-expands when a connection already exists or when a QR/clipboard import populates the pubkey field - Add 9 new string resources for the new UI copy https://claude.ai/code/session_01QzBq319fsxhrT91bGxws6N --- .../amethyst/ui/note/UpdateZapAmountDialog.kt | 510 ++++++++++++------ amethyst/src/main/res/values/strings.xml | 8 + 2 files changed, 349 insertions(+), 169 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 52859f724..3cc05f8fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -31,6 +31,19 @@ import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -42,12 +55,18 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.ContentPaste +import androidx.compose.material.icons.outlined.RadioButtonUnchecked import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material3.Button @@ -55,7 +74,10 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.InputChip +import androidx.compose.material3.InputChipDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -75,7 +97,6 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.components.TextSpinner @@ -88,6 +109,9 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Font14SP +import com.vitorpamplona.amethyst.ui.theme.RowColSpacing +import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier +import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent @@ -103,7 +127,6 @@ fun UpdateZapAmountContent( ) { val context = LocalContext.current val clipboardManager = LocalClipboardManager.current - val uri = LocalUriHandler.current val zapTypes = @@ -155,46 +178,82 @@ fun UpdateZapAmountContent( } } + var qrScanning by remember { mutableStateOf(false) } + + // Expand manual config automatically when a wallet connection exists + var showManualConfig by remember { mutableStateOf(postViewModel.walletConnectPubkey.text.isNotBlank()) } + LaunchedEffect(postViewModel.walletConnectPubkey.text) { + if (postViewModel.walletConnectPubkey.text.isNotBlank()) { + showManualConfig = true + } + } + Column( modifier = Modifier - .padding(10.dp) + .padding(horizontal = 16.dp, vertical = 8.dp) .fillMaxWidth() .imePadding() .verticalScroll(rememberScrollState()), ) { + // ── Section 1: Quick Zap Amounts ────────────────────────────────────── + + Text( + text = stringRes(R.string.quick_zap_amounts), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleSmall, + modifier = SettingsCategoryFirstModifier, + ) + Text( + text = stringRes(R.string.quick_zap_amounts_explainer), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.placeholderText, + modifier = Modifier.padding(bottom = 12.dp), + ) + + // Amount chips — animateContentSize gives a smooth expand/collapse when + // chips are added or removed FlowRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, + modifier = + Modifier + .fillMaxWidth() + .animateContentSize(animationSpec = spring(stiffness = Spring.StiffnessMediumLow)), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), ) { postViewModel.amountSet.forEach { amountInSats -> - Button( - modifier = Modifier.padding(horizontal = 3.dp), - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), + InputChip( + selected = false, onClick = { postViewModel.removeAmount(amountInSats) }, - ) { - Text( - "⚡ ${ - showAmount( - amountInSats.toBigDecimal().setScale(1), - ) - } ✖", - color = Color.White, - textAlign = TextAlign.Center, - ) - } + label = { + Text( + text = "⚡ ${showAmount(amountInSats.toBigDecimal().setScale(1))}", + ) + }, + trailingIcon = { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringRes(R.string.remove), + modifier = Modifier.size(InputChipDefaults.AvatarSize), + ) + }, + colors = + InputChipDefaults.inputChipColors( + containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f), + labelColor = MaterialTheme.colorScheme.primary, + trailingIconColor = MaterialTheme.colorScheme.primary, + ), + ) } } - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(12.dp)) + // Add new amount Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { OutlinedTextField( label = { Text(text = stringRes(R.string.new_amount_in_sats)) }, @@ -212,7 +271,7 @@ fun UpdateZapAmountContent( ) }, singleLine = true, - modifier = Modifier.padding(end = 10.dp).weight(1f), + modifier = Modifier.weight(1f), ) Button( @@ -223,67 +282,159 @@ fun UpdateZapAmountContent( containerColor = MaterialTheme.colorScheme.primary, ), ) { + Icon( + imageVector = Icons.Outlined.Add, + contentDescription = stringRes(R.string.add), + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(4.dp)) Text(text = stringRes(R.string.add), color = Color.White) } } - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - TextSpinner( - label = stringRes(id = R.string.zap_type_explainer), - placeholder = zapTypes.firstOrNull { it.first == accountViewModel.defaultZapType() }?.second ?: zapTypes.firstOrNull()?.second ?: "", - options = zapOptions, - onSelect = { postViewModel.selectedZapType = zapTypes[it].first }, - modifier = Modifier.weight(1f).padding(end = 5.dp), - ) - } + // ── Section 2: Zap Privacy ──────────────────────────────────────────── - HorizontalDivider( - modifier = Modifier.padding(vertical = 10.dp), - thickness = DividerThickness, + Text( + text = stringRes(R.string.zap_privacy_section), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleSmall, + modifier = SettingsCategorySpacingModifier, + ) + Text( + text = stringRes(R.string.zap_type_section_explainer), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.placeholderText, + modifier = Modifier.padding(bottom = 8.dp), ) - - var qrScanning by remember { mutableStateOf(false) } Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - Text( - stringRes(id = R.string.wallet_connect_service), - Modifier.weight(1f), + TextSpinner( + label = stringRes(id = R.string.zap_type_explainer), + placeholder = + zapTypes + .firstOrNull { it.first == accountViewModel.defaultZapType() } + ?.second + ?: zapTypes.firstOrNull()?.second + ?: "", + options = zapOptions, + onSelect = { postViewModel.selectedZapType = zapTypes[it].first }, + modifier = Modifier.fillMaxWidth(), ) + } - IconButton( + // ── Section 3: Nostr Wallet Connect ─────────────────────────────────── + + HorizontalDivider( + modifier = Modifier.padding(vertical = 16.dp), + thickness = DividerThickness, + ) + + // Section header + connection status indicator + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = RowColSpacing, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringRes(R.string.wallet_connect_service), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleSmall, + ) + Text( + text = stringRes(R.string.wallet_connect_service_explainer), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.placeholderText, + modifier = Modifier.padding(top = 2.dp), + ) + } + } + + // Animated connection status badge + val isConnected = postViewModel.walletConnectPubkey.text.isNotBlank() + val statusColor by animateColorAsState( + targetValue = if (isConnected) Color(0xFF4CAF50) else MaterialTheme.colorScheme.placeholderText, + animationSpec = tween(durationMillis = 400), + label = "nwc_status_color", + ) + + AnimatedContent( + targetState = isConnected, + transitionSpec = { fadeIn(tween(300)) togetherWith fadeOut(tween(300)) }, + label = "nwc_status_badge", + ) { connected -> + Row( + modifier = Modifier.padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + imageVector = if (connected) Icons.Filled.CheckCircle else Icons.Outlined.RadioButtonUnchecked, + contentDescription = null, + tint = statusColor, + modifier = Modifier.size(18.dp), + ) + Text( + text = + if (connected) { + stringRes(R.string.wallet_connect_status_connected) + } else { + stringRes(R.string.wallet_connect_status_not_connected) + }, + color = statusColor, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + + // Connect action buttons + Row( + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + // Primary "Connect Wallet" button — opens the NWC app deep link + OutlinedButton( + modifier = Modifier.weight(1f), + shape = ButtonBorder, onClick = { onClose() - try { - uri.openUri("nostrnwc://connect?appname=Amethyst&appicon=https%3A%2F%2Fraw.githubusercontent.com%2Fvitorpamplona%2Famethyst%2Frefs%2Fheads%2Fmain%2Ficon.png&callback=amethyst%2Bwalletconnect%3A%2F%2Fdlnwc") + uri.openUri( + "nostrnwc://connect?appname=Amethyst&appicon=https%3A%2F%2Fraw.githubusercontent.com%2Fvitorpamplona%2Famethyst%2Frefs%2Fheads%2Fmain%2Ficon.png&callback=amethyst%2Bwalletconnect%3A%2F%2Fdlnwc", + ) } catch (_: IllegalArgumentException) { - accountViewModel.toastManager.toast(R.string.couldnt_find_nwc_wallets, R.string.couldnt_find_nwc_wallets_description) + accountViewModel.toastManager.toast( + R.string.couldnt_find_nwc_wallets, + R.string.couldnt_find_nwc_wallets_description, + ) } }, ) { Icon( imageVector = Icons.Outlined.Add, - contentDescription = stringRes(id = R.string.connect_to_new_nwc_wallet), - modifier = Size24Modifier, - tint = MaterialTheme.colorScheme.primary, + contentDescription = null, + modifier = Modifier.size(18.dp), ) + Spacer(modifier = Modifier.width(4.dp)) + Text(text = stringRes(R.string.wallet_connect_connect_app)) } + // Paste from clipboard IconButton( onClick = { - val uri = clipboardManager.getText()?.text + val clipText = clipboardManager.getText()?.text try { - uri?.let { - postViewModel.copyFromClipboard(it) - } + clipText?.let { postViewModel.copyFromClipboard(it) } } catch (e: IllegalArgumentException) { - accountViewModel.toastManager.toast(R.string.invalid_nip47_uri_title, R.string.invalid_nip47_uri_description, uri ?: "") + accountViewModel.toastManager.toast( + R.string.invalid_nip47_uri_title, + R.string.invalid_nip47_uri_description, + clipText ?: "", + ) } }, ) { @@ -295,6 +446,7 @@ fun UpdateZapAmountContent( ) } + // QR code scanner IconButton(onClick = { qrScanning = true }) { Icon( painter = painterRes(R.drawable.ic_qrcode, 3), @@ -305,18 +457,6 @@ fun UpdateZapAmountContent( } } - Row( - modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - stringRes(id = R.string.wallet_connect_service_explainer), - Modifier.weight(1f), - color = MaterialTheme.colorScheme.placeholderText, - fontSize = Font14SP, - ) - } - if (qrScanning) { SimpleQrCodeScanner { qrScanning = false @@ -340,121 +480,153 @@ fun UpdateZapAmountContent( } } + // Expandable manual configuration section Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), + modifier = + Modifier + .fillMaxWidth() + .clickable { showManualConfig = !showManualConfig } + .padding(vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, ) { - OutlinedTextField( - label = { Text(text = stringRes(R.string.wallet_connect_service_pubkey)) }, - value = postViewModel.walletConnectPubkey, - onValueChange = { postViewModel.walletConnectPubkey = it }, - keyboardOptions = - KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.None, - ), - placeholder = { - Text( - text = "npub, hex", - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - singleLine = true, + Text( + text = stringRes(R.string.wallet_connect_manual_config), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.placeholderText, modifier = Modifier.weight(1f), + fontSize = Font14SP, + ) + Icon( + imageVector = if (showManualConfig) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, + contentDescription = null, + tint = MaterialTheme.colorScheme.placeholderText, + modifier = Modifier.size(20.dp), ) } - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, + AnimatedVisibility( + visible = showManualConfig, + enter = expandVertically(animationSpec = spring(stiffness = Spring.StiffnessMediumLow)) + fadeIn(), + exit = shrinkVertically(animationSpec = spring(stiffness = Spring.StiffnessMediumLow)) + fadeOut(), ) { - OutlinedTextField( - label = { Text(text = stringRes(R.string.wallet_connect_service_relay)) }, - modifier = Modifier.weight(1f), - value = postViewModel.walletConnectRelay, - onValueChange = { postViewModel.walletConnectRelay = it }, - placeholder = { - Text( - text = "wss://relay.server.com", - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, + Column { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedTextField( + label = { Text(text = stringRes(R.string.wallet_connect_service_pubkey)) }, + value = postViewModel.walletConnectPubkey, + onValueChange = { postViewModel.walletConnectPubkey = it }, + keyboardOptions = + KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.None, + ), + placeholder = { + Text( + text = "npub, hex", + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), ) - }, - singleLine = true, - ) - } - - var showPassword by remember { mutableStateOf(false) } - - val context = LocalContext.current - - val keyguardLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - if (result.resultCode == Activity.RESULT_OK) { - showPassword = true } - } - val authTitle = stringRes(id = R.string.wallet_connect_service_show_secret) - - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { Text(text = stringRes(R.string.wallet_connect_service_secret)) }, - modifier = Modifier.weight(1f), - value = postViewModel.walletConnectSecret, - onValueChange = { postViewModel.walletConnectSecret = it }, - keyboardOptions = - KeyboardOptions( - autoCorrectEnabled = false, - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Go, - ), - placeholder = { - Text( - text = stringRes(R.string.wallet_connect_service_secret_placeholder), - color = MaterialTheme.colorScheme.placeholderText, + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedTextField( + label = { Text(text = stringRes(R.string.wallet_connect_service_relay)) }, + modifier = Modifier.fillMaxWidth(), + value = postViewModel.walletConnectRelay, + onValueChange = { postViewModel.walletConnectRelay = it }, + placeholder = { + Text( + text = "wss://relay.server.com", + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + ) + }, + singleLine = true, ) - }, - trailingIcon = { - IconButton( - onClick = { - if (!showPassword) { - authenticate( - title = authTitle, - context = context, - keyguardLauncher = keyguardLauncher, - onApproved = { showPassword = true }, - onError = { title, message -> accountViewModel.toastManager.toast(title, message) }, + } + + var showPassword by remember { mutableStateOf(false) } + + val secretContext = LocalContext.current + + val keyguardLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + showPassword = true + } + } + + val authTitle = stringRes(id = R.string.wallet_connect_service_show_secret) + + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedTextField( + label = { Text(text = stringRes(R.string.wallet_connect_service_secret)) }, + modifier = Modifier.fillMaxWidth(), + value = postViewModel.walletConnectSecret, + onValueChange = { postViewModel.walletConnectSecret = it }, + keyboardOptions = + KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Go, + ), + placeholder = { + Text( + text = stringRes(R.string.wallet_connect_service_secret_placeholder), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + trailingIcon = { + IconButton( + onClick = { + if (!showPassword) { + authenticate( + title = authTitle, + context = secretContext, + keyguardLauncher = keyguardLauncher, + onApproved = { showPassword = true }, + onError = { title, message -> accountViewModel.toastManager.toast(title, message) }, + ) + } else { + showPassword = false + } + }, + ) { + Icon( + imageVector = + if (showPassword) { + Icons.Outlined.VisibilityOff + } else { + Icons.Outlined.Visibility + }, + contentDescription = + if (showPassword) { + stringRes(R.string.show_password) + } else { + stringRes(R.string.hide_password) + }, ) - } else { - showPassword = false } }, - ) { - Icon( - imageVector = - if (showPassword) { - Icons.Outlined.VisibilityOff - } else { - Icons.Outlined.Visibility - }, - contentDescription = - if (showPassword) { - stringRes(R.string.show_password) - } else { - stringRes( - R.string.hide_password, - ) - }, - ) - } - }, - visualTransformation = - if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), - ) + visualTransformation = + if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), + ) + } + } } + + Spacer(modifier = Modifier.height(16.dp)) } } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 984ba0245..5d48164b3 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -454,6 +454,14 @@ Wallet Connect Secret Show secret key nsec / hex private key + Connected + Not connected + Advanced: enter connection details manually + Quick Zap Amounts + Shown when long-pressing the zap button. Tap an amount to remove it. + Zap Privacy + Controls how your identity is shown when you send a zap. + Connect Wallet Pledge Amount in Sats Post Poll