From 728ee15e2740b1cc67994216e6bbb664369cf3c2 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 17 Jun 2026 03:11:56 -0700 Subject: [PATCH] Flash message bubble when tapping a reply (#10929) --- src/components/dms/MessageItem.tsx | 46 +++++-------- src/components/dms/MessageItemEmbed.tsx | 67 ++++++++++++------- src/components/dms/MessageItemInviteEmbed.tsx | 26 +++++-- 3 files changed, 81 insertions(+), 58 deletions(-) diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index 2e3cffe997..e8ba7003f2 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -10,6 +10,7 @@ import { import Animated, { FadeIn, FadeOut, + interpolateColor, LayoutAnimationConfig, LinearTransition, useAnimatedStyle, @@ -233,6 +234,15 @@ let MessageItem = ({ const pendingColor = t.palette.primary_300 + const bubbleColor = isFromSelf + ? isPending + ? pendingColor + : t.palette.primary_500 + : t.palette.contrast_50 + const highlightColor = isFromSelf + ? t.palette.primary_300 + : t.palette.primary_100 + const rt = new RichTextAPI({text: message.text, facets: message.facets}) const hasEmbed = @@ -278,7 +288,11 @@ let MessageItem = ({ }, [highlightKey, highlightSV]) const highlightStyle = useAnimatedStyle(() => ({ - opacity: highlightSV.get(), + backgroundColor: interpolateColor( + highlightSV.get(), + [0, 1], + [bubbleColor, highlightColor], + ), })) const borderRadiusStyle = useAnimatedStyle(() => @@ -427,14 +441,6 @@ let MessageItem = ({ web: a.mx_lg, }) - // Negative of `messageInset` so the flash bleeds past the row's horizontal - // margin to the screen edges. - const flashBleed = platform({ - android: -a.mx_sm.marginLeft, - ios: -a.mx_md.marginLeft, - web: -a.mx_lg.marginLeft, - }) - return ( <> {hasLargeGapFromPrev && } @@ -444,20 +450,6 @@ let MessageItem = ({ isFirstInCluster ? a.mt_md : {marginTop: CLUSTERED_MESSAGE_GAP}, hasReactions && {paddingBottom: 26}, ]}> - {showAvatar ? ( {avatar} @@ -508,6 +500,7 @@ let MessageItem = ({ squaredBottomCorner || hasEmbedAndText } squaredTopCorner={squaredTopCorner} + highlightSV={highlightSV} /> )} {ChatBskyEmbedJoinLink.isView(message.embed) && ( @@ -519,6 +512,7 @@ let MessageItem = ({ squaredBottomCorner || hasEmbedAndText } squaredTopCorner={squaredTopCorner} + highlightSV={highlightSV} /> )} {rt.text.length > 0 && ( @@ -535,14 +529,10 @@ let MessageItem = ({ marginTop: hasEmbedAndText ? CLUSTERED_MESSAGE_GAP : 0, - backgroundColor: isFromSelf - ? isPending - ? pendingColor - : t.palette.primary_500 - : t.palette.contrast_50, }, isFromSelf ? a.self_end : a.self_start, borderRadiusStyle, + highlightStyle, ], ]}> {replyTo && !isOnlyEmoji(message.text) ? ( diff --git a/src/components/dms/MessageItemEmbed.tsx b/src/components/dms/MessageItemEmbed.tsx index 3b44947f6c..f65330159e 100644 --- a/src/components/dms/MessageItemEmbed.tsx +++ b/src/components/dms/MessageItemEmbed.tsx @@ -1,5 +1,10 @@ import {memo} from 'react' import {useWindowDimensions, View} from 'react-native' +import Animated, { + interpolateColor, + type SharedValue, + useAnimatedStyle, +} from 'react-native-reanimated' import {type $Typed, type AppBskyEmbedRecord} from '@atproto/api' import {atoms as a, native, useTheme, web} from '#/alf' @@ -15,16 +20,48 @@ let MessageItemEmbed = ({ isGroupChat, squaredTopCorner, squaredBottomCorner, + highlightSV, }: { embed: $Typed isFromSelf: boolean isGroupChat: boolean squaredTopCorner: boolean squaredBottomCorner: boolean + highlightSV: SharedValue }): React.ReactNode => { const t = useTheme() const screen = useWindowDimensions() + const restingColor = isFromSelf ? t.palette.primary_50 : t.palette.contrast_50 + const highlightColor = isFromSelf + ? t.palette.primary_300 + : t.palette.primary_100 + const highlightStyle = useAnimatedStyle(() => ({ + backgroundColor: interpolateColor( + highlightSV.get(), + [0, 1], + [restingColor, highlightColor], + ), + })) + + const radiiStyle = isFromSelf + ? { + borderBottomRightRadius: squaredBottomCorner + ? SQUARED_BORDER_RADIUS + : BORDER_RADIUS, + borderTopRightRadius: squaredTopCorner + ? SQUARED_BORDER_RADIUS + : BORDER_RADIUS, + } + : { + borderBottomLeftRadius: squaredBottomCorner + ? SQUARED_BORDER_RADIUS + : BORDER_RADIUS, + borderTopLeftRadius: squaredTopCorner + ? SQUARED_BORDER_RADIUS + : BORDER_RADIUS, + } + return ( - + - + ) diff --git a/src/components/dms/MessageItemInviteEmbed.tsx b/src/components/dms/MessageItemInviteEmbed.tsx index 049a142af3..5d6acd93d1 100644 --- a/src/components/dms/MessageItemInviteEmbed.tsx +++ b/src/components/dms/MessageItemInviteEmbed.tsx @@ -1,5 +1,10 @@ import {memo} from 'react' import {useWindowDimensions, View} from 'react-native' +import Animated, { + interpolateColor, + type SharedValue, + useAnimatedStyle, +} from 'react-native-reanimated' import {type $Typed, type ChatBskyEmbedJoinLink} from '@atproto/api' import {useConvoActive} from '#/state/messages/convo' @@ -17,17 +22,31 @@ let MessageItemInviteEmbed = ({ isGroupChat, squaredTopCorner, squaredBottomCorner, + highlightSV, }: { embed: $Typed isFromSelf: boolean isGroupChat: boolean squaredTopCorner: boolean squaredBottomCorner: boolean + highlightSV: SharedValue }): React.ReactNode => { const t = useTheme() const screen = useWindowDimensions() const convo = useConvoActive() + const restingColor = isFromSelf ? t.palette.primary_50 : t.palette.contrast_50 + const highlightColor = isFromSelf + ? t.palette.primary_300 + : t.palette.primary_100 + const highlightStyle = useAnimatedStyle(() => ({ + backgroundColor: interpolateColor( + highlightSV.get(), + [0, 1], + [restingColor, highlightColor], + ), + })) + const code = isKnownJoinLinkPreview(embed.joinLinkPreview) ? embed.joinLinkPreview.code : undefined @@ -49,14 +68,13 @@ let MessageItemInviteEmbed = ({ maxWidth: 360, }), ]}> - - + )