From 48485ce933ef18538aaf040d73a41d7e8d5c6bc2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 02:49:07 +0000 Subject: [PATCH 1/2] 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 --- .../amethyst/ui/components/SwipeToDelete.kt | 64 +++++++++++++++++++ .../screen/loggedIn/drafts/DraftListScreen.kt | 4 +- amethyst/src/main/res/values/strings.xml | 1 + 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt index 9f28df570..e64d2534b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt @@ -30,6 +30,7 @@ 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 @@ -38,10 +39,15 @@ import androidx.compose.material3.SwipeToDismissBoxState 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.ui.Alignment import androidx.compose.ui.Modifier 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 fun DismissBackground(dismissState: SwipeToDismissBoxState) { val color by animateColorAsState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt index 110ba8615..fcc2f3eb6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt @@ -48,7 +48,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState 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.RenderFeedContentState import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys.DRAFTS @@ -168,7 +168,7 @@ private fun DraftFeedLoaded( } itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item -> Row(Modifier.fillMaxWidth().animateItem()) { - SwipeToDeleteContainer( + SwipeToDeleteWithConfirmation( modifier = Modifier.fillMaxWidth().animateContentSize(), onStartToEnd = { accountViewModel.delete(item) }, ) { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 72f3724dc..8a710fd46 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1473,6 +1473,7 @@ Add a Blossom Server Delete all Are you sure you want to delete all drafts? + Are you sure you want to delete this draft? " +%1$s" Stack: From b3adc56adf95319e7fc433d3095c02ecd349e278 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 22:11:54 +0000 Subject: [PATCH 2/2] 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 --- .../amethyst/ui/components/SwipeToDelete.kt | 106 ++++++++++-------- .../screen/loggedIn/drafts/DraftListScreen.kt | 2 +- amethyst/src/main/res/values/strings.xml | 1 - 3 files changed, 63 insertions(+), 46 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt index e64d2534b..b2748732e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt @@ -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), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt index fcc2f3eb6..716b4545a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt @@ -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, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 8a710fd46..72f3724dc 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1473,7 +1473,6 @@ Add a Blossom Server Delete all Are you sure you want to delete all drafts? - Are you sure you want to delete this draft? " +%1$s" Stack: