Merge pull request #51 from dmnyc/feat/instant-zap-in-sheet

feat(zap): move instant-zap settings into the zap sheet
This commit is contained in:
Barry Deen
2026-07-04 15:51:18 -04:00
committed by GitHub
2 changed files with 165 additions and 133 deletions
@@ -602,38 +602,85 @@ fun ZapDialog(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.ic_bolt),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(10.dp))
Text(
if (fiatMode) "Instant payments" else "Instant zaps",
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.onSurface
)
Switch(
checked = instantZapsEnabled,
onCheckedChange = {
instantZapsEnabled = it
interfacePrefs.setQuickZapEnabled(it)
},
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = accent,
uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant,
uncheckedTrackColor = MaterialTheme.colorScheme.surface
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.ic_bolt),
contentDescription = null,
tint = if (instantZapsEnabled) accent else MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(16.dp)
)
)
Spacer(Modifier.width(10.dp))
Text(
if (fiatMode) "Instant payments" else "Instant zaps",
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.onSurface
)
Switch(
checked = instantZapsEnabled,
onCheckedChange = {
instantZapsEnabled = it
interfacePrefs.setQuickZapEnabled(it)
},
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = accent,
uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant,
uncheckedTrackColor = MaterialTheme.colorScheme.surface
)
)
}
if (instantZapsEnabled) {
val instantSats = interfacePrefs.getQuickZapAmountSats()
val instantMsg = interfacePrefs.getQuickZapMessage()
HorizontalDivider(
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.18f),
thickness = 0.5.dp
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
"%,d sats".format(instantSats),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold,
color = accent
)
if (instantMsg.isNotBlank()) {
Text(
"·",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
instantMsg,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
}
Text(
"configure in Presets",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
}
}
}
}
@@ -746,10 +793,13 @@ fun ZapDialog(
EditPresetsSheet(
initial = presets,
accent = accent,
interfacePrefs = interfacePrefs,
onDismiss = { showEditPresetsSheet = false },
onSave = { newList ->
zapPrefsRepo.setPresets(newList)
presets = newList.sortedBy { it.amountSats }
// Refresh the in-sheet toggle in case Edit Presets changed it
instantZapsEnabled = interfacePrefs.isQuickZapEnabled()
showEditPresetsSheet = false
}
)
@@ -1041,6 +1091,7 @@ internal fun friendlyZapErrorMessage(raw: String?): String {
private fun EditPresetsSheet(
initial: List<ZapPreset>,
accent: Color,
interfacePrefs: com.darkwisp.app.repo.InterfacePreferences,
onDismiss: () -> Unit,
onSave: (List<ZapPreset>) -> Unit
) {
@@ -1052,6 +1103,13 @@ private fun EditPresetsSheet(
var rows by remember {
mutableStateOf(initial.map { EditableRow(it.amountSats.toString(), it.message) })
}
var instantEnabled by remember { mutableStateOf(interfacePrefs.isQuickZapEnabled()) }
// Track the instant-zap amount by row index so it follows row edits.
var instantIdx by remember {
val savedSats = interfacePrefs.getQuickZapAmountSats()
val idx = initial.indexOfFirst { it.amountSats == savedSats }
mutableStateOf(if (idx >= 0) idx else 0)
}
fun closeSheet(commit: Boolean) {
scope.launch { sheetState.hide() }.invokeOnCompletion {
if (commit) {
@@ -1059,6 +1117,14 @@ private fun EditPresetsSheet(
val sats = r.amount.toLongOrNull() ?: return@mapNotNull null
if (sats <= 0) null else ZapPreset(sats, r.message.trim())
}
// Persist instant-zap settings — the selected preset becomes
// the amount/message fired on long-press.
interfacePrefs.setQuickZapEnabled(instantEnabled)
val safeIdx = instantIdx.coerceIn(0, (parsed.size - 1).coerceAtLeast(0))
parsed.getOrNull(safeIdx)?.let { preset ->
interfacePrefs.setQuickZapAmountSats(preset.amountSats)
interfacePrefs.setQuickZapMessage(preset.message)
}
onSave(parsed)
} else {
onDismiss()
@@ -1103,6 +1169,51 @@ private fun EditPresetsSheet(
borderColor = accent.copy(alpha = 0.45f)
)
}
Spacer(Modifier.height(16.dp))
// ── Instant zap toggle ──────────────────────────────────
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.ic_bolt),
contentDescription = null,
tint = if (instantEnabled) accent else MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
"Instant zaps",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
Text(
"Long-press the bolt to send the selected preset instantly",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = instantEnabled,
onCheckedChange = { instantEnabled = it },
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = accent,
uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant,
uncheckedTrackColor = MaterialTheme.colorScheme.surface
)
)
}
}
Spacer(Modifier.height(12.dp))
Surface(
@@ -1122,10 +1233,12 @@ private fun EditPresetsSheet(
// row doesn't leak its dismiss state into the next
// row sliding into its position.
val rowKey = remember { java.util.UUID.randomUUID().toString() }
val isInstantRow = instantEnabled && idx == instantIdx
val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = { target ->
if (target == SwipeToDismissBoxValue.EndToStart) {
rows = rows.toMutableList().also { it.removeAt(idx) }
if (instantIdx >= rows.size) instantIdx = (rows.size - 1).coerceAtLeast(0)
true
} else false
}
@@ -1221,6 +1334,18 @@ private fun EditPresetsSheet(
},
modifier = Modifier.weight(1f)
)
// Radio button — tap to designate the instant-zap preset
androidx.compose.material3.RadioButton(
selected = isInstantRow,
onClick = { if (instantEnabled) instantIdx = idx },
enabled = instantEnabled,
colors = androidx.compose.material3.RadioButtonDefaults.colors(
selectedColor = accent,
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f),
disabledSelectedColor = accent.copy(alpha = 0.4f),
disabledUnselectedColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.15f)
)
)
}
}
}
@@ -725,110 +725,17 @@ fun InterfaceScreen(
Spacer(Modifier.height(24.dp))
// ── Instant zaps / Instant payments ─────────────────────────────
// Hold-to-zap on the post-card fires the configured amount
// immediately when enabled; tap still opens the composer.
var quickZapEnabled by remember { mutableStateOf(interfacePrefs.isQuickZapEnabled()) }
var quickZapSats by remember { mutableStateOf(interfacePrefs.getQuickZapAmountSats().toString()) }
var quickZapFiat by remember { mutableStateOf(interfacePrefs.getQuickZapAmountFiat().toString()) }
var quickZapMessage by remember { mutableStateOf(interfacePrefs.getQuickZapMessage()) }
Text(
if (fiatModeEnabled) "Payments" else "Zaps",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
if (fiatModeEnabled) "Instant payments" else "Instant zaps",
style = MaterialTheme.typography.bodyMedium
)
Text(
if (fiatModeEnabled)
"Hold a post's pay button to send the configured amount instantly. Tap still opens the composer."
else
"Hold a post's zap bolt to send the configured amount instantly. Tap still opens the composer.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
Switch(
checked = quickZapEnabled,
onCheckedChange = {
quickZapEnabled = it
interfacePrefs.setQuickZapEnabled(it)
},
colors = wispSwitchColors()
)
}
}
if (quickZapEnabled) {
Spacer(Modifier.height(12.dp))
if (fiatModeEnabled) {
OutlinedTextField(
value = quickZapFiat,
onValueChange = { raw ->
// Allow only digits + a single dot. Trim leading zeros.
val cleaned = raw.filter { it.isDigit() || it == '.' }
.let { s ->
val firstDot = s.indexOf('.')
if (firstDot < 0) s
else s.substring(0, firstDot + 1) +
s.substring(firstDot + 1).filter { it != '.' }
}
quickZapFiat = cleaned
cleaned.toDoubleOrNull()?.let {
interfacePrefs.setQuickZapAmountFiat(it)
}
},
label = { Text("Amount (${fiatCurrency})") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
} else {
OutlinedTextField(
value = quickZapSats,
onValueChange = { raw ->
val digits = raw.filter { it.isDigit() }.trimStart('0').ifEmpty { "" }
quickZapSats = digits
// Hard cap at QUICK_ZAP_MAX_SATS so instant zaps
// never bypass the soft-confirmation dialog.
val parsed = digits.toLongOrNull()?.coerceIn(1L, InterfacePreferences.QUICK_ZAP_MAX_SATS)
if (parsed != null) interfacePrefs.setQuickZapAmountSats(parsed)
},
label = { Text("Amount (sats)") },
supportingText = { Text("Max ${InterfacePreferences.QUICK_ZAP_MAX_SATS}") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
}
Spacer(Modifier.height(8.dp))
OutlinedTextField(
value = quickZapMessage,
onValueChange = {
quickZapMessage = it
interfacePrefs.setQuickZapMessage(it)
},
label = { Text("Message (optional)") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
}
Spacer(Modifier.height(24.dp))
// Zap icon toggle — hidden in fiat mode since the bolt is forced
// Zap icon toggle — hidden in fiat mode since the bolt is forced.
// Instant-zap configuration lives in the zap sheet (Edit Presets),
// not here.
var zapBoltIcon by remember { mutableStateOf(interfacePrefs.isZapBoltIcon()) }
if (!fiatModeEnabled) {
Text(
"Zaps",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically