|
1 | 1 | package com.flipcash.app.messenger.internal.screens.components |
2 | 2 |
|
| 3 | +import androidx.compose.animation.AnimatedVisibility |
| 4 | +import androidx.compose.animation.EnterTransition |
3 | 5 | import androidx.compose.animation.core.animateFloatAsState |
4 | | -import androidx.compose.animation.core.spring |
| 6 | +import com.flipcash.app.messenger.internal.screens.ChatAnimations |
5 | 7 | import androidx.compose.foundation.gestures.detectTapGestures |
6 | 8 | import androidx.compose.foundation.layout.Arrangement |
7 | 9 | import androidx.compose.foundation.layout.Box |
@@ -139,22 +141,19 @@ internal fun MessageList( |
139 | 141 | val isOutgoing = (item as? ChatListItem.ContentBubble)?.isFromSelf ?: false |
140 | 142 |
|
141 | 143 | // Message insertion animation — scale from 0.95 + opacity with edge anchor. |
142 | | - // Tuned to match observed iOS timing (~500ms gradual fade-in). |
143 | 144 | // Only animate genuinely new messages (index 0 after initial load). |
144 | 145 | val shouldAnimate = index == 0 && hasLoaded && item.itemKey !in animatedKeys |
145 | 146 | if (shouldAnimate) animatedKeys.add(item.itemKey) |
146 | 147 | var appeared by remember(item.itemKey) { mutableStateOf(!shouldAnimate) } |
147 | 148 | LaunchedEffect(Unit) { if (!appeared) appeared = true } |
148 | | - val insertionAlphaSpec = spring<Float>(dampingRatio = 0.86f, stiffness = 80f) |
149 | | - val insertionScaleSpec = spring<Float>(dampingRatio = 0.73f, stiffness = 300f) |
150 | 149 | val insertionAlpha by animateFloatAsState( |
151 | 150 | targetValue = if (appeared) 1f else 0f, |
152 | | - animationSpec = insertionAlphaSpec, |
| 151 | + animationSpec = ChatAnimations.insertion, |
153 | 152 | label = "insertAlpha", |
154 | 153 | ) |
155 | 154 | val insertionScale by animateFloatAsState( |
156 | 155 | targetValue = if (appeared) 1f else 0.95f, |
157 | | - animationSpec = insertionScaleSpec, |
| 156 | + animationSpec = ChatAnimations.insertion, |
158 | 157 | label = "insertScale", |
159 | 158 | ) |
160 | 159 |
|
@@ -206,12 +205,18 @@ internal fun MessageList( |
206 | 205 | } |
207 | 206 | val showReceipt = |
208 | 207 | shouldShowReceiptLabel(index, item, messages, otherReadPointer) |
209 | | - if (showReceipt && effectiveStatus != null) { |
210 | | - ReceiptLabel( |
211 | | - status = effectiveStatus, |
212 | | - readPointer = otherReadPointer, |
213 | | - animateEntrance = wasSending, |
214 | | - ) |
| 208 | + AnimatedVisibility( |
| 209 | + visible = showReceipt && effectiveStatus != null, |
| 210 | + enter = EnterTransition.None, |
| 211 | + exit = ChatAnimations.receiptExit, |
| 212 | + ) { |
| 213 | + if (effectiveStatus != null) { |
| 214 | + ReceiptLabel( |
| 215 | + status = effectiveStatus, |
| 216 | + readPointer = otherReadPointer, |
| 217 | + animateEntrance = wasSending, |
| 218 | + ) |
| 219 | + } |
215 | 220 | } |
216 | 221 | } |
217 | 222 | } |
@@ -286,20 +291,17 @@ internal fun MessageList( |
286 | 291 | } |
287 | 292 | } |
288 | 293 |
|
289 | | - // opts out of the list maintaining |
290 | | - // scroll position when adding elements before the first item |
291 | | - // we are checking first visible item index to ensure |
292 | | - // the list doesn't shift when viewing scroll back |
| 294 | + // Opt out of the list maintaining scroll position when adding |
| 295 | + // elements before the first item. Only needed during initial load |
| 296 | + // (to prevent starting at the ContactInfoContainer) and when |
| 297 | + // scrolled back (to prevent shifting when pagination prepends). |
| 298 | + // When at index 0, we do NOT call requestScrollToItem — doing so |
| 299 | + // forces an instant reposition that causes a single-frame jitter |
| 300 | + // when new messages are inserted. Instead, animateScrollToItem in |
| 301 | + // the LaunchedEffect above handles smooth scrolling to new items. |
293 | 302 | Snapshot.withoutReadObservation { |
294 | 303 | if (!hasLoaded) { |
295 | | - // During initial load, always pin to index 0 (newest message) |
296 | | - // to prevent the list from starting at the ContactInfoContainer |
297 | 304 | listState.requestScrollToItem(0, 0) |
298 | | - } else if (listState.firstVisibleItemIndex == 0) { |
299 | | - listState.requestScrollToItem( |
300 | | - index = listState.firstVisibleItemIndex, |
301 | | - scrollOffset = listState.firstVisibleItemScrollOffset |
302 | | - ) |
303 | 305 | } |
304 | 306 | } |
305 | 307 | } |
|
0 commit comments