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 facfb3f56..bb7bbfe20 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 @@ -24,8 +24,10 @@ import android.content.Context import android.content.Intent import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ContentTransform import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn @@ -147,6 +149,7 @@ import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.roundToInt @@ -1326,24 +1329,52 @@ 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), - onDismissRequest = { onDismiss() }, + onDismissRequest = { visibilityState.targetState = false }, properties = PopupProperties(focusable = true), ) { - ReactionChoicePopupContent( - reactions, - toRemove = toRemove, - onClick = { reactionType -> - accountViewModel.reactToOrDelete( - baseNote, - reactionType, - ) - onDismiss() - }, - onChangeAmount, - ) + AnimatedVisibility( + visibleState = visibilityState, + enter = + slideInVertically( + initialOffsetY = { it / 2 }, + ) + fadeIn(animationSpec = fadeAnimationSpec), + exit = + slideOutVertically( + targetOffsetY = { it / 2 }, + ) + fadeOut(animationSpec = fadeAnimationSpec), + ) { + ReactionChoicePopupContent( + reactions, + toRemove = toRemove, + onClick = { reactionType -> + accountViewModel.reactToOrDelete( + baseNote, + reactionType, + ) + visibilityState.targetState = false + }, + onChangeAmount, + ) + } } } @@ -1505,59 +1536,87 @@ fun ZapAmountChoicePopup( 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), - onDismissRequest = { onDismiss() }, + onDismissRequest = { visibilityState.targetState = false }, properties = PopupProperties(focusable = true), ) { - FlowRow(horizontalArrangement = Arrangement.Center) { - zapAmountChoices.forEach { amountInSats -> - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = { - accountViewModel.zap( - baseNote, - amountInSats * 1000, - null, - zapMessage, - context, - true, - onError, - onProgress, - onPayViaIntent, - ) - onDismiss() - }, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - ) { - Text( - "⚡ ${showAmount(amountInSats.toBigDecimal().setScale(1))}", - color = Color.White, - textAlign = TextAlign.Center, - modifier = - Modifier.combinedClickable( - onClick = { - accountViewModel.zap( - baseNote, - amountInSats * 1000, - null, - zapMessage, - context, - true, - onError, - onProgress, - onPayViaIntent, - ) - onDismiss() - }, - onLongClick = { onChangeAmount() }, + AnimatedVisibility( + visibleState = visibilityState, + enter = + slideInVertically( + initialOffsetY = { it / 2 }, + ) + fadeIn(animationSpec = fadeAnimationSpec), + exit = + slideOutVertically( + targetOffsetY = { it / 2 }, + ) + fadeOut(animationSpec = fadeAnimationSpec), + ) { + FlowRow(horizontalArrangement = Arrangement.Center) { + zapAmountChoices.forEach { amountInSats -> + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + accountViewModel.zap( + baseNote, + amountInSats * 1000, + null, + zapMessage, + context, + true, + onError, + onProgress, + onPayViaIntent, + ) + visibilityState.targetState = false + }, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, ), - ) + ) { + Text( + "⚡ ${showAmount(amountInSats.toBigDecimal().setScale(1))}", + color = Color.White, + textAlign = TextAlign.Center, + modifier = + Modifier.combinedClickable( + onClick = { + accountViewModel.zap( + baseNote, + amountInSats * 1000, + null, + zapMessage, + context, + true, + onError, + onProgress, + onPayViaIntent, + ) + visibilityState.targetState = false + }, + onLongClick = { onChangeAmount() }, + ), + ) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChannelFabColumn.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChannelFabColumn.kt index 5cdc10772..6f0934618 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChannelFabColumn.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chatrooms/ChannelFabColumn.kt @@ -20,6 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -38,6 +44,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R @@ -76,46 +83,56 @@ fun ChannelFabColumn( } Column { - if (isOpen) { - FloatingActionButton( - onClick = { - wantsToSendNewMessage = true - isOpen = false - }, - modifier = Size55Modifier, - shape = CircleShape, - containerColor = MaterialTheme.colorScheme.primary, - ) { - Text( - text = stringRes(R.string.messages_new_message), - color = Color.White, - textAlign = TextAlign.Center, - fontSize = Font12SP, - ) + AnimatedVisibility( + visible = isOpen, + enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), + ) { + Column { + FloatingActionButton( + onClick = { + wantsToSendNewMessage = true + isOpen = false + }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Text( + text = stringRes(R.string.messages_new_message), + color = Color.White, + textAlign = TextAlign.Center, + fontSize = Font12SP, + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + FloatingActionButton( + onClick = { + wantsToCreateChannel = true + isOpen = false + }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Text( + text = stringRes(R.string.messages_create_public_chat), + color = Color.White, + textAlign = TextAlign.Center, + fontSize = Font12SP, + ) + } + + Spacer(modifier = Modifier.height(20.dp)) } - - Spacer(modifier = Modifier.height(20.dp)) - - FloatingActionButton( - onClick = { - wantsToCreateChannel = true - isOpen = false - }, - modifier = Size55Modifier, - shape = CircleShape, - containerColor = MaterialTheme.colorScheme.primary, - ) { - Text( - text = stringRes(R.string.messages_create_public_chat), - color = Color.White, - textAlign = TextAlign.Center, - fontSize = Font12SP, - ) - } - - Spacer(modifier = Modifier.height(20.dp)) } + val rotationDegree by animateFloatAsState( + targetValue = if (isOpen) 45f else 0f, + ) + FloatingActionButton( onClick = { isOpen = !isOpen }, modifier = Size55Modifier, @@ -125,7 +142,10 @@ fun ChannelFabColumn( Icon( imageVector = Icons.Outlined.Add, contentDescription = stringRes(R.string.messages_create_public_private_chat_description), - modifier = Modifier.size(26.dp), + modifier = + Modifier.size(26.dp).graphicsLayer { + rotationZ = rotationDegree + }, tint = Color.White, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryView.kt index 9e434a3c0..7e16002b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryView.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -73,7 +78,11 @@ fun SummaryBar(state: NotificationSummaryState) { UserReactionsRow(state) { showChart = !showChart } - if (showChart) { + AnimatedVisibility( + visible = showChart, + enter = slideInVertically() + expandVertically(), + exit = slideOutVertically() + shrinkVertically(), + ) { val lineChartCount = lineChart( lines = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewImageButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewImageButton.kt index 60110f49a..7d6e960c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewImageButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewImageButton.kt @@ -25,7 +25,12 @@ import android.net.Uri import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -36,6 +41,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AddPhotoAlternate import androidx.compose.material.icons.filled.CameraAlt +import androidx.compose.material.icons.outlined.Close import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -186,44 +192,51 @@ fun NewImageButton( ShowProgress(postViewModel) } else { Column { - if (isOpen) { - FloatingActionButton( - onClick = { - wantsToPostFromCamera = true - isOpen = false - }, - modifier = Size55Modifier, - shape = CircleShape, - containerColor = MaterialTheme.colorScheme.primary, - ) { - Icon( - imageVector = Icons.Default.CameraAlt, - contentDescription = stringRes(id = R.string.upload_image), - modifier = Modifier.size(26.dp), - tint = Color.White, - ) +// if (isOpen) { + AnimatedVisibility( + visible = isOpen, + enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), + ) { + Column { + FloatingActionButton( + onClick = { + wantsToPostFromCamera = true + isOpen = false + }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Icon( + imageVector = Icons.Default.CameraAlt, + contentDescription = stringRes(id = R.string.upload_image), + modifier = Modifier.size(26.dp), + tint = Color.White, + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + FloatingActionButton( + onClick = { + wantsToPostFromGallery = true + isOpen = false + }, + modifier = Size55Modifier, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Icon( + imageVector = Icons.Default.AddPhotoAlternate, + contentDescription = stringRes(id = R.string.upload_image), + modifier = Modifier.size(26.dp), + tint = Color.White, + ) + } + + Spacer(modifier = Modifier.height(20.dp)) } - - Spacer(modifier = Modifier.height(20.dp)) - - FloatingActionButton( - onClick = { - wantsToPostFromGallery = true - isOpen = false - }, - modifier = Size55Modifier, - shape = CircleShape, - containerColor = MaterialTheme.colorScheme.primary, - ) { - Icon( - imageVector = Icons.Default.AddPhotoAlternate, - contentDescription = stringRes(id = R.string.upload_image), - modifier = Modifier.size(26.dp), - tint = Color.White, - ) - } - - Spacer(modifier = Modifier.height(20.dp)) } FloatingActionButton( @@ -234,12 +247,31 @@ fun NewImageButton( shape = CircleShape, containerColor = MaterialTheme.colorScheme.primary, ) { - Icon( - painter = painterResource(R.drawable.ic_compose), - contentDescription = stringRes(id = R.string.new_short), - modifier = Modifier.size(26.dp), - tint = Color.White, - ) + AnimatedVisibility( + visible = isOpen, + enter = fadeIn(), + exit = fadeOut(), + ) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringRes(id = R.string.new_short), + modifier = Modifier.size(26.dp), + tint = Color.White, + ) + } + + AnimatedVisibility( + visible = !isOpen, + enter = fadeIn(), + exit = fadeOut(), + ) { + Icon( + painter = painterResource(R.drawable.ic_compose), + contentDescription = stringRes(id = R.string.new_short), + modifier = Modifier.size(26.dp), + tint = Color.White, + ) + } } } }