From 935414811bbe15aa9b6ba38aa6c3dec993954722 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:04:53 +0100 Subject: [PATCH 1/2] Improve exit animation of the selected message menu --- .../selectedmessage/SelectedMessageMenu.kt | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/selectedmessage/SelectedMessageMenu.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/selectedmessage/SelectedMessageMenu.kt index 763dcb20566..c60618ae2ce 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/selectedmessage/SelectedMessageMenu.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/selectedmessage/SelectedMessageMenu.kt @@ -19,7 +19,7 @@ package io.getstream.chat.android.compose.ui.components.selectedmessage import android.os.Build import android.view.WindowManager import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.EaseOutCubic +import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -37,6 +37,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds @@ -70,8 +71,10 @@ import io.getstream.chat.android.previewdata.PreviewMessageData import io.getstream.chat.android.previewdata.PreviewUserData import io.getstream.chat.android.ui.common.state.messages.MessageAction import io.getstream.chat.android.ui.common.state.messages.list.MessageItemState +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Represents the options user can take after selecting a message. @@ -114,8 +117,25 @@ public fun SelectedMessageMenu( MessageAlignment.End -> Modifier.padding(end = 8.dp) } + val isInspection = LocalInspectionMode.current + val animation = rememberMenuAnimation( + sourceBounds = LocalSelectedMessageBounds.current?.value, + messageAlignment = messageAlignment, + ) + val scope = rememberCoroutineScope() + val animatedDismiss: () -> Unit = remember(animation, onDismiss, scope) { + { + scope.launch { + withContext(NonCancellable) { + animation.animateOut() + onDismiss() + } + } + } + } + Dialog( - onDismissRequest = onDismiss, + onDismissRequest = animatedDismiss, properties = DialogProperties( usePlatformDefaultWidth = false, decorFitsSystemWindows = false, @@ -129,14 +149,9 @@ public fun SelectedMessageMenu( } } window.setDimAmount(DimAmount) + window.setWindowAnimations(0) } - val isInspection = LocalInspectionMode.current - val animation = rememberMenuAnimation( - sourceBounds = LocalSelectedMessageBounds.current?.value, - messageAlignment = messageAlignment, - ) - LaunchedEffect(Unit) { if (isInspection) animation.snapIn() else animation.animateIn() } @@ -145,7 +160,7 @@ public fun SelectedMessageMenu( modifier = modifier .semantics { testTagsAsResourceId = true } .fillMaxSize() - .clickable(onClick = onDismiss, indication = null, interactionSource = null) + .clickable(onClick = animatedDismiss, indication = null, interactionSource = null) .verticalScroll(rememberScrollState()) .systemBarsPadding() .padding(StreamTokens.spacingXs), @@ -193,7 +208,7 @@ public fun SelectedMessageMenu( Spacer( modifier = Modifier .matchParentSize() - .clickable(onClick = onDismiss, indication = null, interactionSource = null), + .clickable(onClick = animatedDismiss, indication = null, interactionSource = null), ) } @@ -233,7 +248,7 @@ private class MenuAnimationState( val messageModifier: Modifier get() = Modifier .onGloballyPositioned { coords -> - if (coords.isAttached && targetBounds == null) { + if (coords.isAttached) { targetBounds = coords.boundsInWindow() } } @@ -258,8 +273,17 @@ private class MenuAnimationState( suspend fun animateIn() { coroutineScope { - launch { message.animateTo(1f, tween(durationMillis = 300, easing = EaseOutCubic)) } - launch { peripheral.animateTo(1f, tween(durationMillis = 200, delayMillis = 150)) } + launch { message.animateTo(1f, tween(durationMillis = 250, easing = FastOutSlowInEasing)) } + launch { peripheral.animateTo(1f, tween(durationMillis = 150, delayMillis = 120)) } + } + } + + suspend fun animateOut() { + coroutineScope { + launch { peripheral.animateTo(0f, tween(durationMillis = 100)) } + launch { + message.animateTo(0f, tween(durationMillis = 250, delayMillis = 60, easing = FastOutSlowInEasing)) + } } } From b1027a6c59908f445eb0b34c8f5882b44d52ff61 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:40:26 +0100 Subject: [PATCH 2/2] Ensure dismissal only happens once --- .../selectedmessage/SelectedMessageMenu.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/selectedmessage/SelectedMessageMenu.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/selectedmessage/SelectedMessageMenu.kt index c60618ae2ce..a933092336d 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/selectedmessage/SelectedMessageMenu.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/selectedmessage/SelectedMessageMenu.kt @@ -90,7 +90,7 @@ import kotlinx.coroutines.withContext * @param onShowMoreReactionsSelected Handler that propagates clicks on the show more reactions button. * @param modifier Modifier for styling. * @param currentUser The currently logged-in user, used to build the message preview. - * @param onDismiss Handler called when the menu is dismissed. + * @param onDismiss Handler invoked asynchronously once after the exit animation completes. */ @Suppress("LongMethod") @Composable @@ -124,11 +124,16 @@ public fun SelectedMessageMenu( ) val scope = rememberCoroutineScope() val animatedDismiss: () -> Unit = remember(animation, onDismiss, scope) { - { - scope.launch { - withContext(NonCancellable) { - animation.animateOut() - onDismiss() + var dismissed = false + + block@{ + if (!dismissed) { + dismissed = true + scope.launch { + withContext(NonCancellable) { + animation.animateOut() + onDismiss() + } } } }