From 372eaabb5043f249530b962c67090ff090572fea Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 27 Jun 2024 12:13:53 -0400 Subject: [PATCH] Swipe to delete drafts. --- .../amethyst/ui/components/SwipeToDelete.kt | 113 ++++++++++++++++++ .../amethyst/ui/note/elements/DropDownMenu.kt | 6 +- .../ui/screen/RememberForeverStates.kt | 7 +- .../ui/screen/loggedIn/DraftListScreen.kt | 79 ++++++++++-- 4 files changed, 193 insertions(+), 12 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt 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 new file mode 100644 index 000000000..6d542110e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.components + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +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.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.SwipeToDismissBox +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.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SwipeToDeleteContainer( + modifier: Modifier = Modifier, + onStartToEnd: () -> Unit, + onEndToStart: () -> Unit, + content: @Composable (RowScope.() -> Unit), +) { + val dismissState = + rememberSwipeToDismissBoxState( + confirmValueChange = { + when (it) { + StartToEnd -> { + onStartToEnd() + } + EndToStart -> { + onEndToStart() + } + Settled -> return@rememberSwipeToDismissBoxState false + } + return@rememberSwipeToDismissBoxState true + }, + ) + + SwipeToDismissBox( + state = dismissState, + modifier = modifier, + backgroundContent = { DismissBackground(dismissState) }, + content = content, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DismissBackground(dismissState: SwipeToDismissBoxState) { + val color by animateColorAsState( + if (dismissState.currentValue > dismissState.targetValue) { + Color(0xFFFF1744) + } else { + Color.Transparent + }, + label = "DismissBackground", + ) + + Row( + modifier = + Modifier + .fillMaxSize() + .background(color) + .padding(20.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Icon( + Icons.Default.Delete, + contentDescription = stringRes(id = R.string.request_deletion), + ) + Spacer(modifier = Modifier) + Icon( + Icons.Default.Delete, + contentDescription = stringRes(id = R.string.request_deletion), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index 374f8bc8a..ef758e62a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -361,10 +361,8 @@ fun NoteDropDownMenu( DropdownMenuItem( text = { Text(stringRes(R.string.request_deletion)) }, onClick = { - scope.launch(Dispatchers.IO) { - accountViewModel.delete(note) - onDismiss() - } + accountViewModel.delete(note) + onDismiss() }, ) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt index d4c0f453e..b47a2fec0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt @@ -33,7 +33,10 @@ import kotlin.math.roundToInt private val savedScrollStates = mutableMapOf() -private data class ScrollState(val index: Int, val scrollOffsetFraction: Float) +private data class ScrollState( + val index: Int, + val scrollOffsetFraction: Float, +) object ScrollStateKeys { const val GLOBAL_SCREEN = "Global" @@ -43,6 +46,8 @@ object ScrollStateKeys { val HOME_FOLLOWS = Route.Home.base + "Follows" val HOME_REPLIES = Route.Home.base + "FollowsReplies" + val DRAFTS = Route.Home.base + "Drafts" + val DISCOVER_CONTENT = Route.Home.base + "DiscoverContent" val DISCOVER_MARKETPLACE = Route.Home.base + "Marketplace" val DISCOVER_LIVE = Route.Home.base + "Live" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DraftListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DraftListScreen.kt index 0a502c666..c249cc878 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DraftListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DraftListScreen.kt @@ -20,15 +20,34 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteContainer +import com.vitorpamplona.amethyst.ui.note.NoteCompose +import com.vitorpamplona.amethyst.ui.screen.FeedState import com.vitorpamplona.amethyst.ui.screen.NostrDraftEventsFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.ui.screen.RefresheableBox +import com.vitorpamplona.amethyst.ui.screen.RenderFeedState +import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState +import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys.DRAFTS +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.FeedPadding @Composable fun DraftListScreen( @@ -72,10 +91,56 @@ private fun RenderDraftListScreen( onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) } } - RefresheableFeedView( - feedViewModel, - null, - accountViewModel = accountViewModel, - nav = nav, - ) + RefresheableBox(feedViewModel) { + SaveableFeedState(feedViewModel, DRAFTS) { listState -> + RenderFeedState( + feedViewModel, + accountViewModel, + listState, + nav, + null, + onLoaded = { DraftFeedLoaded(it, listState, null, accountViewModel, nav) }, + ) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun DraftFeedLoaded( + state: FeedState.Loaded, + listState: LazyListState, + routeForLastRead: String?, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + LazyColumn( + contentPadding = FeedPadding, + state = listState, + ) { + itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> + SwipeToDeleteContainer( + modifier = Modifier.fillMaxWidth().animateContentSize(), + onStartToEnd = { accountViewModel.delete(item) }, + onEndToStart = { accountViewModel.delete(item) }, + ) { + Row(Modifier.fillMaxWidth().animateItemPlacement().background(MaterialTheme.colorScheme.background)) { + NoteCompose( + item, + routeForLastRead = routeForLastRead, + modifier = Modifier.fillMaxWidth(), + isBoostedNote = false, + isHiddenFeed = state.showHidden.value, + quotesLeft = 3, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + + HorizontalDivider( + thickness = DividerThickness, + ) + } + } }