[Chat] Restructure MessageItem to prevent alignment issues (#10888)
This commit is contained in:
@@ -14,7 +14,6 @@ export function ActionsWrapper({
|
|||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
message: ChatBskyConvoDefs.MessageView
|
message: ChatBskyConvoDefs.MessageView
|
||||||
hasReactions?: boolean
|
|
||||||
isFromSelf: boolean
|
isFromSelf: boolean
|
||||||
senderProfile?: bsky.profile.AnyProfileView
|
senderProfile?: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts | undefined
|
moderationOpts: ModerationOpts | undefined
|
||||||
|
|||||||
@@ -19,14 +19,12 @@ import {canReact, hasReachedReactionLimit} from './util'
|
|||||||
|
|
||||||
export function ActionsWrapper({
|
export function ActionsWrapper({
|
||||||
message,
|
message,
|
||||||
hasReactions,
|
|
||||||
isFromSelf,
|
isFromSelf,
|
||||||
senderProfile,
|
senderProfile,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
message: ChatBskyConvoDefs.MessageView
|
message: ChatBskyConvoDefs.MessageView
|
||||||
hasReactions?: boolean
|
|
||||||
isFromSelf: boolean
|
isFromSelf: boolean
|
||||||
senderProfile?: bsky.profile.AnyProfileView
|
senderProfile?: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts | undefined
|
moderationOpts: ModerationOpts | undefined
|
||||||
@@ -113,7 +111,6 @@ export function ActionsWrapper({
|
|||||||
isFromSelf
|
isFromSelf
|
||||||
? [a.mr_xs, {marginLeft: 'auto'}, a.flex_row_reverse]
|
? [a.mr_xs, {marginLeft: 'auto'}, a.flex_row_reverse]
|
||||||
: [a.ml_xs, {marginRight: 'auto'}],
|
: [a.ml_xs, {marginRight: 'auto'}],
|
||||||
hasReactions ? [a.mb_2xl] : undefined,
|
|
||||||
]}>
|
]}>
|
||||||
{reactionsAvailable && (
|
{reactionsAvailable && (
|
||||||
<EmojiReactionPicker message={message} onEmojiSelect={onEmojiSelect}>
|
<EmojiReactionPicker message={message} onEmojiSelect={onEmojiSelect}>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
type ViewStyle,
|
type ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
|
FadeIn,
|
||||||
|
FadeOut,
|
||||||
LayoutAnimationConfig,
|
LayoutAnimationConfig,
|
||||||
LinearTransition,
|
LinearTransition,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
@@ -285,12 +287,15 @@ let MessageItem = ({
|
|||||||
const appliedReactions = (
|
const appliedReactions = (
|
||||||
<LayoutAnimationConfig skipEntering skipExiting>
|
<LayoutAnimationConfig skipEntering skipExiting>
|
||||||
{hasReactions ? (
|
{hasReactions ? (
|
||||||
<View
|
<Animated.View
|
||||||
|
entering={FadeIn}
|
||||||
|
exiting={FadeOut}
|
||||||
style={[
|
style={[
|
||||||
a.relative,
|
a.absolute,
|
||||||
a.bottom_0,
|
{top: '100%'},
|
||||||
isFromSelf ? [a.align_end] : [a.ml_sm, a.align_start],
|
isFromSelf ? [a.right_0] : [a.left_0, isGroupChat && a.ml_sm],
|
||||||
a.px_sm,
|
a.px_sm,
|
||||||
|
a.z_10,
|
||||||
]}>
|
]}>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessible={true}
|
accessible={true}
|
||||||
@@ -302,24 +307,22 @@ let MessageItem = ({
|
|||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.gap_2xs,
|
a.gap_2xs,
|
||||||
isFromSelf ? a.justify_end : a.justify_start,
|
isFromSelf ? a.justify_end : a.justify_start,
|
||||||
a.flex_wrap,
|
|
||||||
a.rounded_lg,
|
a.rounded_lg,
|
||||||
a.border,
|
a.border,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
t.atoms.shadow_xs,
|
t.atoms.shadow_xs,
|
||||||
|
a.px_sm,
|
||||||
hasSelfReacted
|
hasSelfReacted
|
||||||
? {backgroundColor: t.palette.primary_100}
|
? {backgroundColor: t.palette.primary_100}
|
||||||
: t.atoms.bg_contrast_25,
|
: t.atoms.bg_contrast_25,
|
||||||
{
|
{
|
||||||
paddingTop: platform({android: 2, default: 3}),
|
paddingTop: platform({android: 2, default: 3}),
|
||||||
paddingBottom: platform({android: 2, default: 3}),
|
paddingBottom: platform({android: 2, default: 3}),
|
||||||
paddingLeft: 6,
|
transform: [{translateY: -6}],
|
||||||
paddingRight: 6,
|
|
||||||
transform: [{translateY: -8}],
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={isGroupChat ? () => openReactions(message) : undefined}>
|
onPress={isGroupChat ? () => openReactions(message) : undefined}>
|
||||||
{groupedReactions.map(group => (
|
{groupedReactions.slice(0, 10).map(group => (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={native(ZoomIn.springify(200).delay(400))}
|
entering={native(ZoomIn.springify(200).delay(400))}
|
||||||
exiting={
|
exiting={
|
||||||
@@ -340,7 +343,8 @@ let MessageItem = ({
|
|||||||
</Text>
|
</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
))}
|
))}
|
||||||
{groupedReactions.length !== reactions.length &&
|
{(groupedReactions.length !== reactions.length ||
|
||||||
|
groupedReactions.length > 10) &&
|
||||||
reactions.length > 1 ? (
|
reactions.length > 1 ? (
|
||||||
<View style={[a.p_2xs, a.pl_0, a.justify_center]}>
|
<View style={[a.p_2xs, a.pl_0, a.justify_center]}>
|
||||||
<Text
|
<Text
|
||||||
@@ -357,7 +361,7 @@ let MessageItem = ({
|
|||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</Animated.View>
|
||||||
) : null}
|
) : null}
|
||||||
</LayoutAnimationConfig>
|
</LayoutAnimationConfig>
|
||||||
)
|
)
|
||||||
@@ -375,30 +379,15 @@ let MessageItem = ({
|
|||||||
style={[
|
style={[
|
||||||
messageInset,
|
messageInset,
|
||||||
isFirstInCluster ? a.mt_md : {marginTop: CLUSTERED_MESSAGE_GAP},
|
isFirstInCluster ? a.mt_md : {marginTop: CLUSTERED_MESSAGE_GAP},
|
||||||
|
hasReactions && {paddingBottom: 26},
|
||||||
]}>
|
]}>
|
||||||
<View style={[a.relative]}>
|
<View style={[a.relative]}>
|
||||||
{showAvatar ? (
|
{showAvatar ? (
|
||||||
<View
|
<View style={[a.absolute, a.bottom_0, a.z_50]}>{avatar}</View>
|
||||||
style={[
|
|
||||||
a.absolute,
|
|
||||||
a.bottom_0,
|
|
||||||
a.z_50,
|
|
||||||
hasReactions && {
|
|
||||||
transform: [
|
|
||||||
{
|
|
||||||
translateY: platform({
|
|
||||||
ios: -29,
|
|
||||||
default: -27,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
{avatar}
|
|
||||||
</View>
|
|
||||||
) : null}
|
) : null}
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
a.relative,
|
||||||
a.flex_grow,
|
a.flex_grow,
|
||||||
!isFromSelf && isGroupChat && {paddingLeft: AVATAR_SIZE},
|
!isFromSelf && isGroupChat && {paddingLeft: AVATAR_SIZE},
|
||||||
]}>
|
]}>
|
||||||
@@ -418,81 +407,86 @@ let MessageItem = ({
|
|||||||
{profile && isBlockedOrBlocking(profile) && isGroupChat ? (
|
{profile && isBlockedOrBlocking(profile) && isGroupChat ? (
|
||||||
<BlockedPlaceholder profile={profile} style={borderRadiusStyle} />
|
<BlockedPlaceholder profile={profile} style={borderRadiusStyle} />
|
||||||
) : (
|
) : (
|
||||||
<ActionsWrapper
|
<View style={[a.relative]}>
|
||||||
hasReactions={hasReactions}
|
<ActionsWrapper
|
||||||
isFromSelf={isFromSelf}
|
isFromSelf={isFromSelf}
|
||||||
message={message}
|
message={message}
|
||||||
senderProfile={profile}
|
senderProfile={profile}
|
||||||
moderationOpts={moderationOpts}>
|
moderationOpts={moderationOpts}>
|
||||||
{AppBskyEmbedRecord.isView(message.embed) && (
|
{AppBskyEmbedRecord.isView(message.embed) && (
|
||||||
<MessageItemEmbed
|
<MessageItemEmbed
|
||||||
embed={message.embed}
|
embed={message.embed}
|
||||||
isFromSelf={isFromSelf}
|
isFromSelf={isFromSelf}
|
||||||
isGroupChat={isGroupChat}
|
isGroupChat={isGroupChat}
|
||||||
squaredBottomCorner={squaredBottomCorner || hasEmbedAndText}
|
squaredBottomCorner={
|
||||||
squaredTopCorner={squaredTopCorner}
|
squaredBottomCorner || hasEmbedAndText
|
||||||
/>
|
}
|
||||||
)}
|
squaredTopCorner={squaredTopCorner}
|
||||||
{ChatBskyEmbedJoinLink.isView(message.embed) && (
|
|
||||||
<MessageItemInviteEmbed
|
|
||||||
embed={message.embed}
|
|
||||||
isFromSelf={isFromSelf}
|
|
||||||
isGroupChat={isGroupChat}
|
|
||||||
squaredBottomCorner={squaredBottomCorner || hasEmbedAndText}
|
|
||||||
squaredTopCorner={squaredTopCorner}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{rt.text.length > 0 && (
|
|
||||||
<Animated.View
|
|
||||||
accessibilityHint={l`Double tap or long press the message to add a reaction`}
|
|
||||||
style={[
|
|
||||||
!isFromSelf && isGroupChat && a.ml_sm,
|
|
||||||
!isOnlyEmoji(message.text) && [
|
|
||||||
a.rounded_xl,
|
|
||||||
a.py_sm,
|
|
||||||
a.px_md,
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
],
|
|
||||||
]}>
|
|
||||||
<RichText
|
|
||||||
value={rt}
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
isFromSelf && {color: t.palette.white},
|
|
||||||
// Emoji-only: add top leading to avoid clipping the
|
|
||||||
// glyph, then pull the bottom up by the same amount so
|
|
||||||
// the glyph bottom-aligns with the avatar instead of
|
|
||||||
// sitting above its line-box baseline.
|
|
||||||
isOnlyEmoji(message.text) && [
|
|
||||||
a.leading_tight,
|
|
||||||
// Visually align bottom of the emoji with the avatar
|
|
||||||
!isFromSelf &&
|
|
||||||
platform({
|
|
||||||
android: {marginTop: a.mt_2xs.marginTop},
|
|
||||||
default: {marginBottom: -a.mb_sm.marginBottom},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
]}
|
|
||||||
interactiveStyle={a.underline}
|
|
||||||
enableTags
|
|
||||||
emojiMultiplier={3}
|
|
||||||
shouldProxyLinks={true}
|
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
)}
|
||||||
)}
|
{ChatBskyEmbedJoinLink.isView(message.embed) && (
|
||||||
|
<MessageItemInviteEmbed
|
||||||
|
embed={message.embed}
|
||||||
|
isFromSelf={isFromSelf}
|
||||||
|
isGroupChat={isGroupChat}
|
||||||
|
squaredBottomCorner={
|
||||||
|
squaredBottomCorner || hasEmbedAndText
|
||||||
|
}
|
||||||
|
squaredTopCorner={squaredTopCorner}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{rt.text.length > 0 && (
|
||||||
|
<Animated.View
|
||||||
|
accessibilityHint={l`Double tap or long press the message to add a reaction`}
|
||||||
|
style={[
|
||||||
|
!isFromSelf && isGroupChat && a.ml_sm,
|
||||||
|
!isOnlyEmoji(message.text) && [
|
||||||
|
a.rounded_xl,
|
||||||
|
a.py_sm,
|
||||||
|
a.px_md,
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
],
|
||||||
|
]}>
|
||||||
|
<RichText
|
||||||
|
value={rt}
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
isFromSelf && {color: t.palette.white},
|
||||||
|
// Emoji-only: add top leading to avoid clipping the
|
||||||
|
// glyph, then pull the bottom up by the same amount so
|
||||||
|
// the glyph bottom-aligns with the avatar instead of
|
||||||
|
// sitting above its line-box baseline.
|
||||||
|
isOnlyEmoji(message.text) && [
|
||||||
|
a.leading_tight,
|
||||||
|
// Visually align bottom of the emoji with the avatar
|
||||||
|
!isFromSelf &&
|
||||||
|
platform({
|
||||||
|
android: {marginTop: a.mt_2xs.marginTop},
|
||||||
|
default: {marginBottom: -a.mb_sm.marginBottom},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]}
|
||||||
|
interactiveStyle={a.underline}
|
||||||
|
enableTags
|
||||||
|
emojiMultiplier={3}
|
||||||
|
shouldProxyLinks={true}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</ActionsWrapper>
|
||||||
{appliedReactions}
|
{appliedReactions}
|
||||||
</ActionsWrapper>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
Reference in New Issue
Block a user