Flash message bubble when tapping a reply (#10929)

This commit is contained in:
DS Boyce
2026-06-17 03:11:56 -07:00
committed by GitHub
parent 6af477bcc2
commit 728ee15e27
3 changed files with 81 additions and 58 deletions
+18 -28
View File
@@ -10,6 +10,7 @@ import {
import Animated, { import Animated, {
FadeIn, FadeIn,
FadeOut, FadeOut,
interpolateColor,
LayoutAnimationConfig, LayoutAnimationConfig,
LinearTransition, LinearTransition,
useAnimatedStyle, useAnimatedStyle,
@@ -233,6 +234,15 @@ let MessageItem = ({
const pendingColor = t.palette.primary_300 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 rt = new RichTextAPI({text: message.text, facets: message.facets})
const hasEmbed = const hasEmbed =
@@ -278,7 +288,11 @@ let MessageItem = ({
}, [highlightKey, highlightSV]) }, [highlightKey, highlightSV])
const highlightStyle = useAnimatedStyle(() => ({ const highlightStyle = useAnimatedStyle(() => ({
opacity: highlightSV.get(), backgroundColor: interpolateColor(
highlightSV.get(),
[0, 1],
[bubbleColor, highlightColor],
),
})) }))
const borderRadiusStyle = useAnimatedStyle(() => const borderRadiusStyle = useAnimatedStyle(() =>
@@ -427,14 +441,6 @@ let MessageItem = ({
web: a.mx_lg, web: a.mx_lg,
}) })
// Negative of `messageInset` so the flash bleeds past the row's horizontal
// margin to the screen edges.
const flashBleed = platform<number>({
android: -a.mx_sm.marginLeft,
ios: -a.mx_md.marginLeft,
web: -a.mx_lg.marginLeft,
})
return ( return (
<> <>
{hasLargeGapFromPrev && <DateDivider date={message.sentAt} />} {hasLargeGapFromPrev && <DateDivider date={message.sentAt} />}
@@ -444,20 +450,6 @@ let MessageItem = ({
isFirstInCluster ? a.mt_md : {marginTop: CLUSTERED_MESSAGE_GAP}, isFirstInCluster ? a.mt_md : {marginTop: CLUSTERED_MESSAGE_GAP},
hasReactions && {paddingBottom: 26}, hasReactions && {paddingBottom: 26},
]}> ]}>
<Animated.View
pointerEvents="none"
style={[
a.absolute,
{
top: -CLUSTERED_MESSAGE_GAP,
bottom: -CLUSTERED_MESSAGE_GAP,
left: flashBleed,
right: flashBleed,
backgroundColor: t.palette.primary_100,
},
highlightStyle,
]}
/>
<View style={[a.relative]}> <View style={[a.relative]}>
{showAvatar ? ( {showAvatar ? (
<View style={[a.absolute, a.bottom_0, a.z_50]}>{avatar}</View> <View style={[a.absolute, a.bottom_0, a.z_50]}>{avatar}</View>
@@ -508,6 +500,7 @@ let MessageItem = ({
squaredBottomCorner || hasEmbedAndText squaredBottomCorner || hasEmbedAndText
} }
squaredTopCorner={squaredTopCorner} squaredTopCorner={squaredTopCorner}
highlightSV={highlightSV}
/> />
)} )}
{ChatBskyEmbedJoinLink.isView(message.embed) && ( {ChatBskyEmbedJoinLink.isView(message.embed) && (
@@ -519,6 +512,7 @@ let MessageItem = ({
squaredBottomCorner || hasEmbedAndText squaredBottomCorner || hasEmbedAndText
} }
squaredTopCorner={squaredTopCorner} squaredTopCorner={squaredTopCorner}
highlightSV={highlightSV}
/> />
)} )}
{rt.text.length > 0 && ( {rt.text.length > 0 && (
@@ -535,14 +529,10 @@ let MessageItem = ({
marginTop: hasEmbedAndText marginTop: hasEmbedAndText
? CLUSTERED_MESSAGE_GAP ? CLUSTERED_MESSAGE_GAP
: 0, : 0,
backgroundColor: isFromSelf
? isPending
? pendingColor
: t.palette.primary_500
: t.palette.contrast_50,
}, },
isFromSelf ? a.self_end : a.self_start, isFromSelf ? a.self_end : a.self_start,
borderRadiusStyle, borderRadiusStyle,
highlightStyle,
], ],
]}> ]}>
{replyTo && !isOnlyEmoji(message.text) ? ( {replyTo && !isOnlyEmoji(message.text) ? (
+41 -26
View File
@@ -1,5 +1,10 @@
import {memo} from 'react' import {memo} from 'react'
import {useWindowDimensions, View} from 'react-native' 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 {type $Typed, type AppBskyEmbedRecord} from '@atproto/api'
import {atoms as a, native, useTheme, web} from '#/alf' import {atoms as a, native, useTheme, web} from '#/alf'
@@ -15,16 +20,48 @@ let MessageItemEmbed = ({
isGroupChat, isGroupChat,
squaredTopCorner, squaredTopCorner,
squaredBottomCorner, squaredBottomCorner,
highlightSV,
}: { }: {
embed: $Typed<AppBskyEmbedRecord.View> embed: $Typed<AppBskyEmbedRecord.View>
isFromSelf: boolean isFromSelf: boolean
isGroupChat: boolean isGroupChat: boolean
squaredTopCorner: boolean squaredTopCorner: boolean
squaredBottomCorner: boolean squaredBottomCorner: boolean
highlightSV: SharedValue<number>
}): React.ReactNode => { }): React.ReactNode => {
const t = useTheme() const t = useTheme()
const screen = useWindowDimensions() 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 ( return (
<MessageContextProvider> <MessageContextProvider>
<View <View
@@ -44,37 +81,15 @@ let MessageItemEmbed = ({
// CLUSTERED_MESSAGE_GAP (2px) is the only spacing applied // CLUSTERED_MESSAGE_GAP (2px) is the only spacing applied
{marginTop: -a.mt_sm.marginTop}, {marginTop: -a.mt_sm.marginTop},
]}> ]}>
<View> <Animated.View
style={[a.rounded_xl, a.overflow_hidden, radiiStyle, highlightStyle]}>
<Embed <Embed
embed={embed} embed={embed}
allowNestedQuotes allowNestedQuotes
viewContext={PostEmbedViewContext.ChatMessage} viewContext={PostEmbedViewContext.ChatMessage}
style={[ style={[a.rounded_xl, a.overflow_hidden, a.border_0, radiiStyle]}
a.rounded_xl,
a.overflow_hidden,
a.border_0,
isFromSelf
? {
backgroundColor: t.palette.primary_50,
borderBottomRightRadius: squaredBottomCorner
? SQUARED_BORDER_RADIUS
: BORDER_RADIUS,
borderTopRightRadius: squaredTopCorner
? SQUARED_BORDER_RADIUS
: BORDER_RADIUS,
}
: {
backgroundColor: t.palette.contrast_50,
borderBottomLeftRadius: squaredBottomCorner
? SQUARED_BORDER_RADIUS
: BORDER_RADIUS,
borderTopLeftRadius: squaredTopCorner
? SQUARED_BORDER_RADIUS
: BORDER_RADIUS,
},
]}
/> />
</View> </Animated.View>
</View> </View>
</MessageContextProvider> </MessageContextProvider>
) )
+22 -4
View File
@@ -1,5 +1,10 @@
import {memo} from 'react' import {memo} from 'react'
import {useWindowDimensions, View} from 'react-native' 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 {type $Typed, type ChatBskyEmbedJoinLink} from '@atproto/api'
import {useConvoActive} from '#/state/messages/convo' import {useConvoActive} from '#/state/messages/convo'
@@ -17,17 +22,31 @@ let MessageItemInviteEmbed = ({
isGroupChat, isGroupChat,
squaredTopCorner, squaredTopCorner,
squaredBottomCorner, squaredBottomCorner,
highlightSV,
}: { }: {
embed: $Typed<ChatBskyEmbedJoinLink.View> embed: $Typed<ChatBskyEmbedJoinLink.View>
isFromSelf: boolean isFromSelf: boolean
isGroupChat: boolean isGroupChat: boolean
squaredTopCorner: boolean squaredTopCorner: boolean
squaredBottomCorner: boolean squaredBottomCorner: boolean
highlightSV: SharedValue<number>
}): React.ReactNode => { }): React.ReactNode => {
const t = useTheme() const t = useTheme()
const screen = useWindowDimensions() const screen = useWindowDimensions()
const convo = useConvoActive() 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) const code = isKnownJoinLinkPreview(embed.joinLinkPreview)
? embed.joinLinkPreview.code ? embed.joinLinkPreview.code
: undefined : undefined
@@ -49,14 +68,13 @@ let MessageItemInviteEmbed = ({
maxWidth: 360, maxWidth: 360,
}), }),
]}> ]}>
<View <Animated.View
style={[ style={[
a.p_md, a.p_md,
a.gap_md, a.gap_md,
a.overflow_hidden, a.overflow_hidden,
isFromSelf isFromSelf
? { ? {
backgroundColor: t.palette.primary_50,
borderBottomRightRadius: squaredBottomCorner borderBottomRightRadius: squaredBottomCorner
? SQUARED_BORDER_RADIUS ? SQUARED_BORDER_RADIUS
: BORDER_RADIUS, : BORDER_RADIUS,
@@ -67,7 +85,6 @@ let MessageItemInviteEmbed = ({
borderTopLeftRadius: BORDER_RADIUS, borderTopLeftRadius: BORDER_RADIUS,
} }
: { : {
backgroundColor: t.palette.contrast_50,
borderBottomLeftRadius: squaredBottomCorner borderBottomLeftRadius: squaredBottomCorner
? SQUARED_BORDER_RADIUS ? SQUARED_BORDER_RADIUS
: BORDER_RADIUS, : BORDER_RADIUS,
@@ -77,6 +94,7 @@ let MessageItemInviteEmbed = ({
borderBottomRightRadius: BORDER_RADIUS, borderBottomRightRadius: BORDER_RADIUS,
borderTopRightRadius: BORDER_RADIUS, borderTopRightRadius: BORDER_RADIUS,
}, },
highlightStyle,
]}> ]}>
<ChatInvite.Root <ChatInvite.Root
code={code} code={code}
@@ -85,7 +103,7 @@ let MessageItemInviteEmbed = ({
hasFixedHeight={false}> hasFixedHeight={false}>
<MessageItemInviteEmbedBody /> <MessageItemInviteEmbedBody />
</ChatInvite.Root> </ChatInvite.Root>
</View> </Animated.View>
</View> </View>
</MessageContextProvider> </MessageContextProvider>
) )