From d7c18341cdf98e9def71df58a686188a243a2015 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 15 Oct 2024 11:58:40 -0400 Subject: [PATCH] Uses the same Reaction animation to also do Boosts --- .../amethyst/ui/note/ReactionsRow.kt | 271 +++++++++++------- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 6 + 2 files changed, 169 insertions(+), 108 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index bb7bbfe20..08a5366a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -26,6 +26,8 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ContentTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.animateFloatAsState @@ -110,6 +112,7 @@ import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.routeToMessage import com.vitorpamplona.amethyst.ui.note.types.EditState +import com.vitorpamplona.amethyst.ui.note.types.RenderReaction import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder @@ -135,6 +138,9 @@ import com.vitorpamplona.amethyst.ui.theme.Size28Modifier import com.vitorpamplona.amethyst.ui.theme.SmallBorder import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.TinyBorders +import com.vitorpamplona.amethyst.ui.theme.defaultTweenDuration +import com.vitorpamplona.amethyst.ui.theme.defaultTweenFloatSpec +import com.vitorpamplona.amethyst.ui.theme.defaultTweenIntOffsetSpec import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.reactionBox @@ -1014,20 +1020,26 @@ fun ZapReaction( interactionSource = remember { MutableInteractionSource() }, indication = ripple24dp, onClick = { - zapClick( - baseNote, - accountViewModel, - context, - onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } }, - onMultipleChoices = { wantsToZap = true }, - onError = { _, message -> - scope.launch { - zappingProgress = 0f - showErrorMessageDialog = showErrorMessageDialog + message - } - }, - onPayViaIntent = { wantsToPay = it }, - ) + scope.launch { + zapClick( + baseNote, + accountViewModel, + context, + onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } }, + onMultipleChoices = { + scope.launch { + wantsToZap = true + } + }, + onError = { _, message -> + scope.launch { + zappingProgress = 0f + showErrorMessageDialog = showErrorMessageDialog + message + } + }, + onPayViaIntent = { wantsToPay = it }, + ) + } }, onLongClick = { wantsToChangeZapAmount = true }, onDoubleClick = { wantsToSetCustomZap = true }, @@ -1043,8 +1055,10 @@ fun ZapReaction( zappingProgress = 0f }, onChangeAmount = { - wantsToZap = false - wantsToChangeZapAmount = true + scope.launch { + wantsToZap = false + wantsToChangeZapAmount = true + } }, onError = { _, message -> scope.launch { @@ -1249,7 +1263,6 @@ fun ObserveZapAmountText( } } -@OptIn(ExperimentalLayoutApi::class) @Composable private fun BoostTypeChoicePopup( baseNote: Note, @@ -1259,62 +1272,120 @@ private fun BoostTypeChoicePopup( onQuote: () -> Unit, onRepost: () -> Unit, onFork: () -> Unit, +) { + val visibilityState = rememberVisibilityState(onDismiss) + BoostTypeChoicePopup( + baseNote, + iconSize, + accountViewModel, + visibilityState, + onQuote, + onRepost, + onFork, + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun BoostTypeChoicePopup( + baseNote: Note, + iconSize: Dp, + accountViewModel: AccountViewModel, + visibilityState: MutableTransitionState, + onQuote: () -> Unit, + onRepost: () -> Unit, + onFork: () -> Unit, ) { val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() } Popup( alignment = Alignment.BottomCenter, offset = IntOffset(0, iconSizePx), - onDismissRequest = { onDismiss() }, + onDismissRequest = { visibilityState.targetState = false }, ) { - FlowRow { - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = { - if (accountViewModel.isWriteable()) { - accountViewModel.boost(baseNote) - onDismiss() - } else { - onRepost() - onDismiss() - } - }, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - ) { - Text(stringRes(R.string.boost), color = Color.White, textAlign = TextAlign.Center) - } + AnimatedVisibility( + visibleState = visibilityState, + enter = popupAnimationEnter, + exit = popupAnimationExit, + ) { + FlowRow { + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + if (accountViewModel.isWriteable()) { + accountViewModel.boost(baseNote) + visibilityState.targetState = false + } else { + onRepost() + visibilityState.targetState = false + } + }, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + ) { + Text(stringRes(R.string.boost), color = Color.White, textAlign = TextAlign.Center) + } - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = onQuote, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - ) { - Text(stringRes(R.string.quote), color = Color.White, textAlign = TextAlign.Center) - } + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = onQuote, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + ) { + Text(stringRes(R.string.quote), color = Color.White, textAlign = TextAlign.Center) + } - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = onFork, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - ) { - Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center) + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = onFork, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + ) { + Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center) + } } } } } +val popupAnimationEnter: EnterTransition = + slideInVertically( + animationSpec = defaultTweenIntOffsetSpec, + initialOffsetY = { it / 2 }, + ) + fadeIn(animationSpec = defaultTweenFloatSpec) + +val popupAnimationExit: ExitTransition = + slideOutVertically( + animationSpec = defaultTweenIntOffsetSpec, + targetOffsetY = { it / 2 }, + ) + fadeOut(animationSpec = defaultTweenFloatSpec) + +@Composable +fun rememberVisibilityState(onDismiss: () -> Unit): MutableTransitionState { + // Prevent multiple calls to onDismiss() + var dismissed by remember { mutableStateOf(false) } + + val visibilityState = remember { MutableTransitionState(false).apply { targetState = true } } + LaunchedEffect(visibilityState.targetState) { + if (!visibilityState.targetState && !dismissed) { + delay(defaultTweenDuration.toLong()) + dismissed = true + onDismiss() + } + } + + return visibilityState +} + @Composable fun ReactionChoicePopup( baseNote: Note, @@ -1322,6 +1393,18 @@ fun ReactionChoicePopup( accountViewModel: AccountViewModel, onDismiss: () -> Unit, onChangeAmount: () -> Unit, +) { + val visibilityState = rememberVisibilityState(onDismiss) + ReactionChoicePopup(baseNote, iconSize, accountViewModel, visibilityState, onChangeAmount) +} + +@Composable +fun ReactionChoicePopup( + baseNote: Note, + iconSize: Dp, + accountViewModel: AccountViewModel, + visibilityState: MutableTransitionState, + onChangeAmount: () -> Unit, ) { val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() } @@ -1329,22 +1412,6 @@ fun ReactionChoicePopup( .collectAsStateWithLifecycle() val toRemove = remember { baseNote.reactedBy(accountViewModel.userProfile()).toImmutableSet() } - // Define animation specs - val animationDuration = 250 - val fadeAnimationSpec = tween(durationMillis = animationDuration) - - // Prevent multiple calls to onDismiss() - var dismissed by remember { mutableStateOf(false) } - - val visibilityState = remember { MutableTransitionState(false).apply { targetState = true } } - LaunchedEffect(visibilityState.targetState) { - if (!visibilityState.targetState && !dismissed) { - delay(animationDuration.toLong()) - dismissed = true - onDismiss() - } - } - Popup( alignment = Alignment.BottomCenter, offset = IntOffset(0, iconSizePx), @@ -1353,14 +1420,8 @@ fun ReactionChoicePopup( ) { AnimatedVisibility( visibleState = visibilityState, - enter = - slideInVertically( - initialOffsetY = { it / 2 }, - ) + fadeIn(animationSpec = fadeAnimationSpec), - exit = - slideOutVertically( - targetOffsetY = { it / 2 }, - ) + fadeOut(animationSpec = fadeAnimationSpec), + enter = popupAnimationEnter, + exit = popupAnimationExit, ) { ReactionChoicePopupContent( reactions, @@ -1518,7 +1579,6 @@ fun ZapAmountChoicePopup( ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent) } -@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @Composable fun ZapAmountChoicePopup( baseNote: Note, @@ -1530,28 +1590,29 @@ fun ZapAmountChoicePopup( onError: (title: String, text: String) -> Unit, onProgress: (percent: Float) -> Unit, onPayViaIntent: (ImmutableList) -> Unit, +) { + val visibilityState = rememberVisibilityState(onDismiss) + ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, visibilityState, onChangeAmount, onError, onProgress, onPayViaIntent) +} + +@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) +@Composable +fun ZapAmountChoicePopup( + baseNote: Note, + zapAmountChoices: ImmutableList, + accountViewModel: AccountViewModel, + popupYOffset: Dp, + visibilityState: MutableTransitionState, + onChangeAmount: () -> Unit, + onError: (title: String, text: String) -> Unit, + onProgress: (percent: Float) -> Unit, + onPayViaIntent: (ImmutableList) -> Unit, ) { val context = LocalContext.current val zapMessage = "" val yOffset = with(LocalDensity.current) { -popupYOffset.toPx().toInt() } - // Define animation specs - val animationDuration = 250 - val fadeAnimationSpec = tween(durationMillis = animationDuration) - - // Prevent multiple calls to onDismiss() - var dismissed by remember { mutableStateOf(false) } - - val visibilityState = remember { MutableTransitionState(false).apply { targetState = true } } - LaunchedEffect(visibilityState.targetState) { - if (!visibilityState.targetState && !dismissed) { - delay(animationDuration.toLong()) - dismissed = true - onDismiss() - } - } - Popup( alignment = Alignment.BottomCenter, offset = IntOffset(0, yOffset), @@ -1560,14 +1621,8 @@ fun ZapAmountChoicePopup( ) { AnimatedVisibility( visibleState = visibilityState, - enter = - slideInVertically( - initialOffsetY = { it / 2 }, - ) + fadeIn(animationSpec = fadeAnimationSpec), - exit = - slideOutVertically( - targetOffsetY = { it / 2 }, - ) + fadeOut(animationSpec = fadeAnimationSpec), + enter = popupAnimationEnter, + exit = popupAnimationExit, ) { FlowRow(horizontalArrangement = Arrangement.Center) { zapAmountChoices.forEach { amountInSats -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 705165b83..0bec3e967 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.theme +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues @@ -40,6 +41,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp val Shapes = @@ -287,3 +289,7 @@ val reactionBox = .padding(5.dp) val ripple24dp = ripple(bounded = false, radius = Size24dp) + +val defaultTweenDuration = 100 +val defaultTweenFloatSpec = tween(durationMillis = defaultTweenDuration) +val defaultTweenIntOffsetSpec = tween(durationMillis = defaultTweenDuration)