Uses the same Reaction animation to also do Boosts

This commit is contained in:
Vitor Pamplona
2024-10-15 11:58:40 -04:00
parent ee16f6c143
commit d7c18341cd
2 changed files with 169 additions and 108 deletions

View File

@@ -26,6 +26,8 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ContentTransform import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateFloatAsState 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.INav
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.types.EditState 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.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder 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.SmallBorder
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.TinyBorders 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.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.reactionBox import com.vitorpamplona.amethyst.ui.theme.reactionBox
@@ -1014,12 +1020,17 @@ fun ZapReaction(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = ripple24dp, indication = ripple24dp,
onClick = { onClick = {
scope.launch {
zapClick( zapClick(
baseNote, baseNote,
accountViewModel, accountViewModel,
context, context,
onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } }, onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } },
onMultipleChoices = { wantsToZap = true }, onMultipleChoices = {
scope.launch {
wantsToZap = true
}
},
onError = { _, message -> onError = { _, message ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
@@ -1028,6 +1039,7 @@ fun ZapReaction(
}, },
onPayViaIntent = { wantsToPay = it }, onPayViaIntent = { wantsToPay = it },
) )
}
}, },
onLongClick = { wantsToChangeZapAmount = true }, onLongClick = { wantsToChangeZapAmount = true },
onDoubleClick = { wantsToSetCustomZap = true }, onDoubleClick = { wantsToSetCustomZap = true },
@@ -1043,8 +1055,10 @@ fun ZapReaction(
zappingProgress = 0f zappingProgress = 0f
}, },
onChangeAmount = { onChangeAmount = {
scope.launch {
wantsToZap = false wantsToZap = false
wantsToChangeZapAmount = true wantsToChangeZapAmount = true
}
}, },
onError = { _, message -> onError = { _, message ->
scope.launch { scope.launch {
@@ -1249,7 +1263,6 @@ fun ObserveZapAmountText(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun BoostTypeChoicePopup( private fun BoostTypeChoicePopup(
baseNote: Note, baseNote: Note,
@@ -1259,13 +1272,41 @@ private fun BoostTypeChoicePopup(
onQuote: () -> Unit, onQuote: () -> Unit,
onRepost: () -> Unit, onRepost: () -> Unit,
onFork: () -> 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<Boolean>,
onQuote: () -> Unit,
onRepost: () -> Unit,
onFork: () -> Unit,
) { ) {
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() } val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
Popup( Popup(
alignment = Alignment.BottomCenter, alignment = Alignment.BottomCenter,
offset = IntOffset(0, iconSizePx), offset = IntOffset(0, iconSizePx),
onDismissRequest = { onDismiss() }, onDismissRequest = { visibilityState.targetState = false },
) {
AnimatedVisibility(
visibleState = visibilityState,
enter = popupAnimationEnter,
exit = popupAnimationExit,
) { ) {
FlowRow { FlowRow {
Button( Button(
@@ -1273,10 +1314,10 @@ private fun BoostTypeChoicePopup(
onClick = { onClick = {
if (accountViewModel.isWriteable()) { if (accountViewModel.isWriteable()) {
accountViewModel.boost(baseNote) accountViewModel.boost(baseNote)
onDismiss() visibilityState.targetState = false
} else { } else {
onRepost() onRepost()
onDismiss() visibilityState.targetState = false
} }
}, },
shape = ButtonBorder, shape = ButtonBorder,
@@ -1313,6 +1354,36 @@ private fun BoostTypeChoicePopup(
} }
} }
} }
}
}
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<Boolean> {
// 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 @Composable
@@ -1322,6 +1393,18 @@ fun ReactionChoicePopup(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onChangeAmount: () -> Unit, onChangeAmount: () -> Unit,
) {
val visibilityState = rememberVisibilityState(onDismiss)
ReactionChoicePopup(baseNote, iconSize, accountViewModel, visibilityState, onChangeAmount)
}
@Composable
fun ReactionChoicePopup(
baseNote: Note,
iconSize: Dp,
accountViewModel: AccountViewModel,
visibilityState: MutableTransitionState<Boolean>,
onChangeAmount: () -> Unit,
) { ) {
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() } val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
@@ -1329,22 +1412,6 @@ fun ReactionChoicePopup(
.collectAsStateWithLifecycle() .collectAsStateWithLifecycle()
val toRemove = remember { baseNote.reactedBy(accountViewModel.userProfile()).toImmutableSet() } val toRemove = remember { baseNote.reactedBy(accountViewModel.userProfile()).toImmutableSet() }
// Define animation specs
val animationDuration = 250
val fadeAnimationSpec = tween<Float>(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( Popup(
alignment = Alignment.BottomCenter, alignment = Alignment.BottomCenter,
offset = IntOffset(0, iconSizePx), offset = IntOffset(0, iconSizePx),
@@ -1353,14 +1420,8 @@ fun ReactionChoicePopup(
) { ) {
AnimatedVisibility( AnimatedVisibility(
visibleState = visibilityState, visibleState = visibilityState,
enter = enter = popupAnimationEnter,
slideInVertically( exit = popupAnimationExit,
initialOffsetY = { it / 2 },
) + fadeIn(animationSpec = fadeAnimationSpec),
exit =
slideOutVertically(
targetOffsetY = { it / 2 },
) + fadeOut(animationSpec = fadeAnimationSpec),
) { ) {
ReactionChoicePopupContent( ReactionChoicePopupContent(
reactions, reactions,
@@ -1518,7 +1579,6 @@ fun ZapAmountChoicePopup(
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent) ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent)
} }
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun ZapAmountChoicePopup( fun ZapAmountChoicePopup(
baseNote: Note, baseNote: Note,
@@ -1530,28 +1590,29 @@ fun ZapAmountChoicePopup(
onError: (title: String, text: String) -> Unit, onError: (title: String, text: String) -> Unit,
onProgress: (percent: Float) -> Unit, onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> 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<Long>,
accountViewModel: AccountViewModel,
popupYOffset: Dp,
visibilityState: MutableTransitionState<Boolean>,
onChangeAmount: () -> Unit,
onError: (title: String, text: String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val zapMessage = "" val zapMessage = ""
val yOffset = with(LocalDensity.current) { -popupYOffset.toPx().toInt() } val yOffset = with(LocalDensity.current) { -popupYOffset.toPx().toInt() }
// Define animation specs
val animationDuration = 250
val fadeAnimationSpec = tween<Float>(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( Popup(
alignment = Alignment.BottomCenter, alignment = Alignment.BottomCenter,
offset = IntOffset(0, yOffset), offset = IntOffset(0, yOffset),
@@ -1560,14 +1621,8 @@ fun ZapAmountChoicePopup(
) { ) {
AnimatedVisibility( AnimatedVisibility(
visibleState = visibilityState, visibleState = visibilityState,
enter = enter = popupAnimationEnter,
slideInVertically( exit = popupAnimationExit,
initialOffsetY = { it / 2 },
) + fadeIn(animationSpec = fadeAnimationSpec),
exit =
slideOutVertically(
targetOffsetY = { it / 2 },
) + fadeOut(animationSpec = fadeAnimationSpec),
) { ) {
FlowRow(horizontalArrangement = Arrangement.Center) { FlowRow(horizontalArrangement = Arrangement.Center) {
zapAmountChoices.forEach { amountInSats -> zapAmountChoices.forEach { amountInSats ->

View File

@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.theme package com.vitorpamplona.amethyst.ui.theme
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues 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.graphics.Color
import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
val Shapes = val Shapes =
@@ -287,3 +289,7 @@ val reactionBox =
.padding(5.dp) .padding(5.dp)
val ripple24dp = ripple(bounded = false, radius = Size24dp) val ripple24dp = ripple(bounded = false, radius = Size24dp)
val defaultTweenDuration = 100
val defaultTweenFloatSpec = tween<Float>(durationMillis = defaultTweenDuration)
val defaultTweenIntOffsetSpec = tween<IntOffset>(durationMillis = defaultTweenDuration)