mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-28 21:53:01 +02:00
Uses the same Reaction animation to also do Boosts
This commit is contained in:
@@ -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 ->
|
||||||
|
@@ -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)
|
||||||
|
Reference in New Issue
Block a user