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,20 +1020,26 @@ fun ZapReaction(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = ripple24dp, indication = ripple24dp,
onClick = { onClick = {
zapClick( scope.launch {
baseNote, zapClick(
accountViewModel, baseNote,
context, accountViewModel,
onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } }, context,
onMultipleChoices = { wantsToZap = true }, onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } },
onError = { _, message -> onMultipleChoices = {
scope.launch { scope.launch {
zappingProgress = 0f wantsToZap = true
showErrorMessageDialog = showErrorMessageDialog + message }
} },
}, onError = { _, message ->
onPayViaIntent = { wantsToPay = it }, scope.launch {
) zappingProgress = 0f
showErrorMessageDialog = showErrorMessageDialog + message
}
},
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 = {
wantsToZap = false scope.launch {
wantsToChangeZapAmount = true wantsToZap = false
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,62 +1272,120 @@ 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 },
) { ) {
FlowRow { AnimatedVisibility(
Button( visibleState = visibilityState,
modifier = Modifier.padding(horizontal = 3.dp), enter = popupAnimationEnter,
onClick = { exit = popupAnimationExit,
if (accountViewModel.isWriteable()) { ) {
accountViewModel.boost(baseNote) FlowRow {
onDismiss() Button(
} else { modifier = Modifier.padding(horizontal = 3.dp),
onRepost() onClick = {
onDismiss() if (accountViewModel.isWriteable()) {
} accountViewModel.boost(baseNote)
}, visibilityState.targetState = false
shape = ButtonBorder, } else {
colors = onRepost()
ButtonDefaults.buttonColors( visibilityState.targetState = false
containerColor = MaterialTheme.colorScheme.primary, }
), },
) { shape = ButtonBorder,
Text(stringRes(R.string.boost), color = Color.White, textAlign = TextAlign.Center) colors =
} ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text(stringRes(R.string.boost), color = Color.White, textAlign = TextAlign.Center)
}
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = onQuote, onClick = onQuote,
shape = ButtonBorder, shape = ButtonBorder,
colors = colors =
ButtonDefaults.buttonColors( ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
), ),
) { ) {
Text(stringRes(R.string.quote), color = Color.White, textAlign = TextAlign.Center) Text(stringRes(R.string.quote), color = Color.White, textAlign = TextAlign.Center)
} }
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = onFork, onClick = onFork,
shape = ButtonBorder, shape = ButtonBorder,
colors = colors =
ButtonDefaults.buttonColors( ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
), ),
) { ) {
Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center) 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<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
fun ReactionChoicePopup( fun ReactionChoicePopup(
baseNote: Note, baseNote: Note,
@@ -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)