feat: replace dialog with swipe-to-reveal delete button for drafts

Instead of showing a confirmation dialog, the swipe now stays open
revealing a red background with a clickable "Request Deletion" button.
Supports swiping from both left-to-right and right-to-left directions.
The user taps the revealed button to confirm deletion.

https://claude.ai/code/session_01JWJxejv2EKrPj2KGEfPPxU
This commit is contained in:
Claude
2026-03-13 22:11:54 +00:00
parent 48485ce933
commit b3adc56adf
3 changed files with 63 additions and 46 deletions
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
@@ -30,7 +31,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -40,14 +40,11 @@ import androidx.compose.material3.SwipeToDismissBoxValue.EndToStart
import androidx.compose.material3.SwipeToDismissBoxValue.Settled
import androidx.compose.material3.SwipeToDismissBoxValue.StartToEnd
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -56,6 +53,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -98,56 +96,26 @@ fun SwipeToDeleteContainer(
@Composable
fun SwipeToDeleteWithConfirmation(
modifier: Modifier = Modifier,
onStartToEnd: () -> Unit,
onDelete: () -> Unit,
content: @Composable (RowScope.() -> Unit),
) {
var showConfirmDialog by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val dismissState =
rememberSwipeToDismissBoxState(
confirmValueChange = {
when (it) {
StartToEnd -> {
showConfirmDialog = true
}
EndToStart -> {}
Settled -> {}
}
return@rememberSwipeToDismissBoxState false
},
positionalThreshold = { it * .40f },
)
if (showConfirmDialog) {
AlertDialog(
onDismissRequest = { showConfirmDialog = false },
title = { Text(text = stringRes(id = R.string.request_deletion)) },
text = { Text(text = stringRes(id = R.string.delete_draft_confirmation)) },
confirmButton = {
TextButton(
onClick = {
showConfirmDialog = false
onStartToEnd()
},
) {
Text(text = stringRes(id = R.string.yes))
}
},
dismissButton = {
TextButton(
onClick = { showConfirmDialog = false },
) {
Text(text = stringRes(id = R.string.no))
}
},
)
}
SwipeToDismissBox(
state = dismissState,
modifier = modifier,
backgroundContent = { DismissBackground(dismissState) },
enableDismissFromEndToStart = false,
backgroundContent = {
ConfirmDeleteBackground(dismissState) {
onDelete()
scope.launch { dismissState.reset() }
}
},
enableDismissFromEndToStart = true,
content = content,
)
}
@@ -191,3 +159,53 @@ fun DismissBackground(dismissState: SwipeToDismissBoxState) {
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConfirmDeleteBackground(
dismissState: SwipeToDismissBoxState,
onConfirmDelete: () -> Unit,
) {
val settled = dismissState.currentValue == Settled && dismissState.targetValue == Settled
val color by animateColorAsState(
if (!settled) {
Color(0xFFFF1744)
} else {
MaterialTheme.colorScheme.surfaceVariant
},
label = "ConfirmDeleteBackground",
)
val haptic = LocalHapticFeedback.current
LaunchedEffect(key1 = dismissState.currentValue > dismissState.targetValue) {
if (dismissState.progress > 0 && dismissState.progress < 1) {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
}
Row(
modifier =
Modifier
.fillMaxSize()
.background(color)
.clickable(enabled = !settled) { onConfirmDelete() }
.padding(20.dp, 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Icon(
Icons.Default.Delete,
contentDescription = stringRes(id = R.string.request_deletion),
)
Text(
text = stringRes(id = R.string.request_deletion),
color = Color.White,
style = MaterialTheme.typography.titleMedium,
)
Icon(
Icons.Default.Delete,
contentDescription = stringRes(id = R.string.request_deletion),
)
}
}
@@ -170,7 +170,7 @@ private fun DraftFeedLoaded(
Row(Modifier.fillMaxWidth().animateItem()) {
SwipeToDeleteWithConfirmation(
modifier = Modifier.fillMaxWidth().animateContentSize(),
onStartToEnd = { accountViewModel.delete(item) },
onDelete = { accountViewModel.delete(item) },
) {
NoteCompose(
item,
-1
View File
@@ -1473,7 +1473,6 @@
<string name="add_a_blossom_server">Add a Blossom Server</string>
<string name="delete_all">Delete all</string>
<string name="delete_all_drafts_confirmation">Are you sure you want to delete all drafts?</string>
<string name="delete_draft_confirmation">Are you sure you want to delete this draft?</string>
<string name="and_more" translatable="false">" +%1$s"</string>
<string name="stack">Stack:</string>