From 5104271e910b19e6a6efb6a82cbad4761d32dc12 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 31 Aug 2026 16:30:33 -0400 Subject: [PATCH 1/2] fix(chat): anchor the send scroll instead of animating to a moving target Sending a message ran animateScrollToItem(0) while the previous message's receipt collapsed its height away above it. The animated scroll resolves its target offset when it starts, so the collapse moved that target mid-flight and the list overshot and corrected, which read as the new bubble bouncing. Own messages now use requestScrollToItem(0, 0), so the new bubble is anchored during the next measure pass, in the same frame the receipt collapses. Incoming messages keep the animated scroll when near the bottom. iOS has no equivalent scroll to race: ChatViewController never computes a content offset by hand, and ChatLayout's keepContentOffsetAtBottomOnBatchUpdates pins the bottom edge across the single batch that inserts the new cell and reconfigures the old one's receipt away. --- .../internal/screens/components/MessageList.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt index cea1b61ad1..f51d6ff5cf 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt @@ -302,12 +302,17 @@ internal fun MessageList( // Always scroll for own messages; only near-bottom for incoming val nearBottom = listState.firstVisibleItemIndex <= 5 val newest = messages.peek(0) as? ChatListItem.ContentBubble - if (newest?.isFromSelf == true || nearBottom) { - if (nearBottom) { - listState.animateScrollToItem(0) - } else { - listState.scrollToItem(0) - } + when { + // Own message: anchor the new bubble during the next measure pass + // rather than animating to it. An animated scroll resolves its target + // offset up front, so the previous message's receipt collapsing out + // from under it moves the target mid-flight and the list overshoots + // and corrects — read as the bubble bouncing. iOS never hand-rolls + // this scroll at all: ChatLayout's keepContentOffsetAtBottomOnBatchUpdates + // pins the bottom edge across the one batch that both inserts the new + // cell and reconfigures the old one's receipt away. + newest?.isFromSelf == true -> listState.requestScrollToItem(0, 0) + nearBottom -> listState.animateScrollToItem(0) } } } From 09d5156e2d45dfd27fb93f520bccc6f7bac6e22c Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 31 Aug 2026 16:30:38 -0400 Subject: [PATCH 2/2] fix(chat): drop the scale from the receipt label's reveal The "Delivered" line grew in with expandVertically() + scaleIn(0.95) + fadeIn. The scale has no counterpart on iOS: ChatColumnCell un-hides a ChatReceiptLabel inside its stack view and cross-fades the text, so the line only ever changes opacity while the cell self-sizes around it. Removes scaleIn from the reveal. The expand, the fade, the Delivered/Read swap, and every spring spec in ChatAnimations are unchanged. --- .../messenger/internal/screens/components/ReceiptLabel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ReceiptLabel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ReceiptLabel.kt index ac76dafc70..fc21283176 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ReceiptLabel.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ReceiptLabel.kt @@ -84,9 +84,10 @@ internal fun ReceiptLabel( ) { AnimatedVisibility( visible = deliveredVisible, + // Expand + opacity, no scale: iOS un-hides the label and cross-fades its text in, + // letting the cell self-size. The line itself never scales. enter = if (animateEntrance) { - expandVertically() + - scaleIn(deliveredSpec, initialScale = 0.95f) + fadeIn(deliveredSpec) + expandVertically() + fadeIn(deliveredSpec) } else { expandVertically(snap()) + fadeIn(snap()) },