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.animation.animateColorAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope 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.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme 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.Settled
import androidx.compose.material3.SwipeToDismissBoxValue.StartToEnd import androidx.compose.material3.SwipeToDismissBoxValue.StartToEnd
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -56,6 +53,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -98,56 +96,26 @@ fun SwipeToDeleteContainer(
@Composable @Composable
fun SwipeToDeleteWithConfirmation( fun SwipeToDeleteWithConfirmation(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onStartToEnd: () -> Unit, onDelete: () -> Unit,
content: @Composable (RowScope.() -> Unit), content: @Composable (RowScope.() -> Unit),
) { ) {
var showConfirmDialog by remember { mutableStateOf(false) } val scope = rememberCoroutineScope()
val dismissState = val dismissState =
rememberSwipeToDismissBoxState( rememberSwipeToDismissBoxState(
confirmValueChange = {
when (it) {
StartToEnd -> {
showConfirmDialog = true
}
EndToStart -> {}
Settled -> {}
}
return@rememberSwipeToDismissBoxState false
},
positionalThreshold = { it * .40f }, 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( SwipeToDismissBox(
state = dismissState, state = dismissState,
modifier = modifier, modifier = modifier,
backgroundContent = { DismissBackground(dismissState) }, backgroundContent = {
enableDismissFromEndToStart = false, ConfirmDeleteBackground(dismissState) {
onDelete()
scope.launch { dismissState.reset() }
}
},
enableDismissFromEndToStart = true,
content = content, 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()) { Row(Modifier.fillMaxWidth().animateItem()) {
SwipeToDeleteWithConfirmation( SwipeToDeleteWithConfirmation(
modifier = Modifier.fillMaxWidth().animateContentSize(), modifier = Modifier.fillMaxWidth().animateContentSize(),
onStartToEnd = { accountViewModel.delete(item) }, onDelete = { accountViewModel.delete(item) },
) { ) {
NoteCompose( NoteCompose(
item, item,
-1
View File
@@ -1473,7 +1473,6 @@
<string name="add_a_blossom_server">Add a Blossom Server</string> <string name="add_a_blossom_server">Add a Blossom Server</string>
<string name="delete_all">Delete all</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_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="and_more" translatable="false">" +%1$s"</string>
<string name="stack">Stack:</string> <string name="stack">Stack:</string>