feat: add confirmation dialog when swiping to delete a draft

The swipe-to-delete gesture on the drafts screen was too sensitive,
causing accidental deletions. Added a confirmation dialog that appears
after the swipe gesture completes, requiring the user to confirm before
the draft is actually deleted.

https://claude.ai/code/session_01JWJxejv2EKrPj2KGEfPPxU
This commit is contained in:
Claude
2026-03-13 02:49:07 +00:00
parent 198aa0971c
commit 48485ce933
3 changed files with 67 additions and 2 deletions
@@ -30,6 +30,7 @@ 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
@@ -38,10 +39,15 @@ import androidx.compose.material3.SwipeToDismissBoxState
import androidx.compose.material3.SwipeToDismissBoxValue.EndToStart 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.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.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
@@ -88,6 +94,64 @@ fun SwipeToDeleteContainer(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SwipeToDeleteWithConfirmation(
modifier: Modifier = Modifier,
onStartToEnd: () -> Unit,
content: @Composable (RowScope.() -> Unit),
) {
var showConfirmDialog by remember { mutableStateOf(false) }
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,
content = content,
)
}
@Composable @Composable
fun DismissBackground(dismissState: SwipeToDismissBoxState) { fun DismissBackground(dismissState: SwipeToDismissBoxState) {
val color by animateColorAsState( val color by animateColorAsState(
@@ -48,7 +48,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteContainer import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteWithConfirmation
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys.DRAFTS import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys.DRAFTS
@@ -168,7 +168,7 @@ private fun DraftFeedLoaded(
} }
itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item -> itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
Row(Modifier.fillMaxWidth().animateItem()) { Row(Modifier.fillMaxWidth().animateItem()) {
SwipeToDeleteContainer( SwipeToDeleteWithConfirmation(
modifier = Modifier.fillMaxWidth().animateContentSize(), modifier = Modifier.fillMaxWidth().animateContentSize(),
onStartToEnd = { accountViewModel.delete(item) }, onStartToEnd = { accountViewModel.delete(item) },
) { ) {
+1
View File
@@ -1473,6 +1473,7 @@
<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>