Update context menu presentation (#10426)

This commit is contained in:
DS Boyce
2026-05-07 13:24:55 -07:00
committed by GitHub
parent 10cb0dbf21
commit 9a2cabf64c
12 changed files with 115 additions and 153 deletions
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20"><path fill="#000" fill-rule="evenodd" d="M14.5 8a1 1 0 0 1 .912.59l4.5 10a1 1 0 0 1-1.824.82L16.554 16h-4.108l-1.534 3.41a1 1 0 0 1-1.824-.82l4.5-10A1 1 0 0 1 14.5 8m-1.153 6h2.306L14.5 11.437zM5.053.106a1 1 0 0 1 1.342.447L7.118 2H11a1 1 0 0 1 0 2H9.92c-.28 1.765-1.27 3.24-2.393 4.398a17 17 0 0 0 2.605 1.562q.129.062.197.092l.005.002.049.022.01.005a1 1 0 0 1-.787 1.838h-.002l-.027-.013-.068-.03-.242-.113A19 19 0 0 1 6 9.75a19 19 0 0 1-3.509 2.125l-.068.03-.027.013h-.002a1 1 0 0 1-.788-1.838l.011-.005.049-.022.005-.002q.069-.03.197-.092a17 17 0 0 0 2.604-1.562C3.348 7.239 2.36 5.764 2.08 4H1a1 1 0 0 1 0-2h3.882l-.277-.553A1 1 0 0 1 5.053.106M4.118 4c.27 1.151.98 2.193 1.882 3.1.901-.907 1.612-1.95 1.882-3.1z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 819 B

+24 -20
View File
@@ -378,7 +378,10 @@ export function Trigger({
<View ref={ref} style={[{opacity: context.isOpen ? 0 : 1}, style]}> <View ref={ref} style={[{opacity: context.isOpen ? 0 : 1}, style]}>
{children({ {children({
IS_NATIVE: true, IS_NATIVE: true,
control: {isOpen: context.isOpen, open}, control: {
isOpen: context.isOpen,
open: mode => void open(mode),
},
state: { state: {
pressed: false, pressed: false,
hovered: false, hovered: false,
@@ -594,10 +597,12 @@ const MENU_WIDTH = 240
export function Outer({ export function Outer({
children, children,
label,
style, style,
align = 'left', align = 'left',
}: { }: {
children: React.ReactNode children: React.ReactNode
label?: string
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
align?: 'left' | 'right' align?: 'left' | 'right'
}) { }) {
@@ -709,23 +714,25 @@ export function Outer({
]}> ]}>
{/* innermost element - needs an overflow: hidden for children, but we also need a shadow, {/* innermost element - needs an overflow: hidden for children, but we also need a shadow,
so put the shadow on the scaling element and the overflow on the innermost element */} so put the shadow on the scaling element and the overflow on the innermost element */}
<View <View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}>
style={[ {label ? (
a.flex_1, <Text
a.rounded_md, numberOfLines={1}
a.overflow_hidden, style={[
a.border, a.pl_md,
t.atoms.border_contrast_low, a.pt_md,
]}> a.pr_lg,
a.pb_md,
a.text_xs,
t.atoms.text_contrast_medium,
]}>
{label}
</Text>
) : null}
{flattenReactChildren(children).map((child, i) => { {flattenReactChildren(children).map((child, i) => {
return isValidElement(child) && return isValidElement(child) &&
(child.type === Item || child.type === Divider) ? ( (child.type === Item || child.type === Divider) ? (
<Fragment key={i}> <Fragment key={i}>
{i > 0 ? (
<View
style={[a.border_b, t.atoms.border_contrast_low]}
/>
) : null}
{cloneElement(child, { {cloneElement(child, {
// @ts-expect-error not typed // @ts-expect-error not typed
style: { style: {
@@ -736,6 +743,7 @@ export function Outer({
</Fragment> </Fragment>
) : null ) : null
})} })}
{label ? <View style={[a.pb_md]} /> : null}
</View> </View>
</Animated.View> </Animated.View>
</Animated.View> </Animated.View>
@@ -840,13 +848,10 @@ export function Item({
!unstyled && [ !unstyled && [
a.flex_row, a.flex_row,
a.align_center, a.align_center,
a.gap_sm, a.px_2xl,
a.px_md,
a.rounded_md, a.rounded_md,
a.border,
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
t.atoms.border_contrast_low, {gap: 6, minHeight: 44, paddingVertical: 10},
{minHeight: 44, paddingVertical: 10},
(focused || pressed || context.hoveredMenuItem === id) && (focused || pressed || context.hoveredMenuItem === id) &&
!rest.disabled && !rest.disabled &&
t.atoms.bg_contrast_50, t.atoms.bg_contrast_50,
@@ -877,7 +882,6 @@ export function ItemText({children, style}: ItemTextProps) {
a.text_md, a.text_md,
a.font_semi_bold, a.font_semi_bold,
t.atoms.text_contrast_high, t.atoms.text_contrast_high,
{paddingTop: 3},
style, style,
disabled && t.atoms.text_contrast_low, disabled && t.atoms.text_contrast_low,
]}> ]}>
+51 -1
View File
@@ -1,6 +1,25 @@
import {type StyleProp, type ViewStyle} from 'react-native'
import {atoms as a, useTheme} from '#/alf'
import * as Menu from '#/components/Menu'
import {Text} from '#/components/Typography'
import {type AuxiliaryViewProps} from './types' import {type AuxiliaryViewProps} from './types'
export * from '#/components/Menu' export {
ContainerItem,
type MenuControlProps as ContextMenuControlProps,
Divider,
Group,
Item,
ItemIcon,
ItemRadio,
ItemText,
LabelText,
Root,
Trigger,
useMenuContext as useContextMenuContext,
useMenuControl as useContextMenuControl,
} from '#/components/Menu'
export function Provider({children}: {children: React.ReactNode}) { export function Provider({children}: {children: React.ReactNode}) {
return children return children
@@ -10,3 +29,34 @@ export function Provider({children}: {children: React.ReactNode}) {
export function AuxiliaryView({}: AuxiliaryViewProps) { export function AuxiliaryView({}: AuxiliaryViewProps) {
return null return null
} }
export function Outer({
children,
label,
style,
}: {
children: React.ReactNode
label?: string
style?: StyleProp<ViewStyle>
}) {
const t = useTheme()
return (
<Menu.Outer style={style}>
{label ? (
<Text
numberOfLines={1}
style={[
a.pl_sm,
a.pt_md,
a.pr_lg,
a.pb_md,
a.text_xs,
t.atoms.text_contrast_medium,
]}>
{label}
</Text>
) : null}
{children}
</Menu.Outer>
)
}
+1 -6
View File
@@ -11,22 +11,17 @@ export function ActionsWrapper({
isFromSelf, isFromSelf,
senderProfile, senderProfile,
children, children,
onTap,
}: { }: {
message: ChatBskyConvoDefs.MessageView message: ChatBskyConvoDefs.MessageView
hasReactions?: boolean hasReactions?: boolean
isFromSelf: boolean isFromSelf: boolean
senderProfile?: bsky.profile.AnyProfileView senderProfile?: bsky.profile.AnyProfileView
children: React.ReactNode children: React.ReactNode
onTap?: () => void
}) { }) {
const {t: l} = useLingui() const {t: l} = useLingui()
return ( return (
<MessageContextMenu <MessageContextMenu message={message} senderProfile={senderProfile}>
message={message}
senderProfile={senderProfile}
onTap={onTap}>
{trigger => {trigger =>
// will always be true, since this file is platform split // will always be true, since this file is platform split
trigger.IS_NATIVE && ( trigger.IS_NATIVE && (
+2 -7
View File
@@ -20,14 +20,12 @@ export function ActionsWrapper({
isFromSelf, isFromSelf,
senderProfile, senderProfile,
children, children,
onTap,
}: { }: {
message: ChatBskyConvoDefs.MessageView message: ChatBskyConvoDefs.MessageView
hasReactions?: boolean hasReactions?: boolean
isFromSelf: boolean isFromSelf: boolean
senderProfile?: bsky.profile.AnyProfileView senderProfile?: bsky.profile.AnyProfileView
children: React.ReactNode children: React.ReactNode
onTap?: () => void
}) { }) {
const viewRef = useRef(null) const viewRef = useRef(null)
const t = useTheme() const t = useTheme()
@@ -140,13 +138,10 @@ export function ActionsWrapper({
}} }}
</MessageContextMenu> </MessageContextMenu>
</View> </View>
<Pressable <View
accessibilityRole="button"
accessibilityHint={l`Click to view the date and time`}
onPress={onTap}
style={[{maxWidth: '80%'}, isFromSelf ? a.align_end : a.align_start]}> style={[{maxWidth: '80%'}, isFromSelf ? a.align_end : a.align_start]}>
{children} {children}
</Pressable> </View>
</View> </View>
) )
} }
-44
View File
@@ -1,44 +0,0 @@
import {createContext, useCallback, useContext, useState} from 'react'
type DateDividerToggleContextType = {
isDividerToggled: (id: string) => boolean
toggleDivider: (id: string) => void
}
const DateDividerToggleContext = createContext<DateDividerToggleContextType>({
isDividerToggled: () => false,
toggleDivider: () => {},
})
export function DateDividerToggleProvider({
children,
}: {
children: React.ReactNode
}) {
const [toggledIds, setToggledIds] = useState(new Set<string>())
const toggleDivider = useCallback((id: string) => {
setToggledIds(prev => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}, [])
const isDividerToggled = useCallback(
(id: string) => toggledIds.has(id),
[toggledIds],
)
return (
<DateDividerToggleContext.Provider
value={{isDividerToggled, toggleDivider}}>
{children}
</DateDividerToggleContext.Provider>
)
}
export function useDateDividerToggle() {
return useContext(DateDividerToggleContext)
}
+4 -16
View File
@@ -11,10 +11,7 @@ import {
useContextMenuContext, useContextMenuContext,
useContextMenuMenuContext, useContextMenuMenuContext,
} from '#/components/ContextMenu/context' } from '#/components/ContextMenu/context'
import { import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
EmojiHeartEyes_Stroke2_Corner0_Rounded as EmojiHeartEyesIcon,
EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon,
} from '#/components/icons/Emoji'
import {type TriggerProps} from '#/components/Menu/types' import {type TriggerProps} from '#/components/Menu/types'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {EmojiPopup} from './EmojiPopup' import {EmojiPopup} from './EmojiPopup'
@@ -37,11 +34,6 @@ export function EmojiReactionPicker({
const [layout, setLayout] = useState({width: 0, height: 0}) const [layout, setLayout] = useState({width: 0, height: 0})
const {width: screenWidth} = useWindowDimensions() const {width: screenWidth} = useWindowDimensions()
// 1 in 100 chance of showing heart eyes icon
const EmojiIcon = useMemo(() => {
return Math.random() < 0.01 ? EmojiHeartEyesIcon : EmojiSmileIcon
}, [])
const position = useMemo(() => { const position = useMemo(() => {
return { return {
x: x:
@@ -76,7 +68,7 @@ export function EmojiReactionPicker({
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
a.shadow_md, a.shadow_md,
]}> ]}>
{['👍', '😆', '❤️', '👀', '😢'].map(emoji => { {['❤️', '👍', '😆', '👀', '😢'].map(emoji => {
const alreadyReacted = hasAlreadyReacted( const alreadyReacted = hasAlreadyReacted(
message, message,
currentAccount?.did, currentAccount?.did,
@@ -123,16 +115,12 @@ export function EmojiReactionPicker({
<View <View
style={[ style={[
a.rounded_full, a.rounded_full,
t.scheme === 'light' t.atoms.bg_contrast_50,
? t.atoms.bg_contrast_25
: t.atoms.bg_contrast_50,
{height: 40, width: 40}, {height: 40, width: 40},
a.justify_center, a.justify_center,
a.align_center, a.align_center,
a.border,
t.atoms.border_contrast_low,
]}> ]}>
<EmojiIcon size="xl" fill={t.palette.contrast_400} /> <PlusIcon size="md" fill={t.palette.contrast_1000} />
</View> </View>
</EmojiPopup> </EmojiPopup>
</View> </View>
@@ -7,7 +7,7 @@ import {DropdownMenu} from 'radix-ui'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {atoms as a, flatten, useTheme} from '#/alf' import {atoms as a, flatten, useTheme} from '#/alf'
import * as EmojiPicker from '#/components/EmojiPicker' import * as EmojiPicker from '#/components/EmojiPicker'
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid' import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import * as Menu from '#/components/Menu' import * as Menu from '#/components/Menu'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {hasAlreadyReacted, hasReachedReactionLimit} from './util' import {hasAlreadyReacted, hasReachedReactionLimit} from './util'
@@ -70,7 +70,7 @@ function MenuInner({
) : ( ) : (
<Menu.Outer style={[a.rounded_full]}> <Menu.Outer style={[a.rounded_full]}>
<View style={[a.flex_row, a.gap_xs]}> <View style={[a.flex_row, a.gap_xs]}>
{['👍', '😆', '❤️', '👀', '😢'].map(emoji => { {['❤️', '👍', '😆', '👀', '😢'].map(emoji => {
const alreadyReacted = hasAlreadyReacted( const alreadyReacted = hasAlreadyReacted(
message, message,
currentAccount?.did, currentAccount?.did,
@@ -118,10 +118,11 @@ function MenuInner({
style={flatten([ style={flatten([
a.rounded_full, a.rounded_full,
{height: 34, width: 34}, {height: 34, width: 34},
t.atoms.bg_contrast_50,
a.justify_center, a.justify_center,
a.align_center, a.align_center,
])}> ])}>
<DotGridIcon size="lg" style={t.atoms.text_contrast_medium} /> <PlusIcon size="md" style={t.atoms.text_contrast_medium} />
</Pressable> </Pressable>
</DropdownMenu.Item> </DropdownMenu.Item>
</View> </View>
+11 -12
View File
@@ -15,10 +15,10 @@ import {atoms as a} from '#/alf'
import * as ContextMenu from '#/components/ContextMenu' import * as ContextMenu from '#/components/ContextMenu'
import {type TriggerProps} from '#/components/ContextMenu/types' import {type TriggerProps} from '#/components/ContextMenu/types'
import {AfterReportDialog} from '#/components/dms/AfterReportDialog' import {AfterReportDialog} from '#/components/dms/AfterReportDialog'
import {BubbleQuestion_Stroke2_Corner0_Rounded as TranslateIcon} from '#/components/icons/Bubble'
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag'
import {Language_Stroke2_Corner2_Rounded as LanguageIcon} from '#/components/icons/Language'
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import {ReportDialog} from '#/components/moderation/ReportDialog' import {ReportDialog} from '#/components/moderation/ReportDialog'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {usePromptControl} from '#/components/Prompt' import {usePromptControl} from '#/components/Prompt'
@@ -33,14 +33,12 @@ export let MessageContextMenu = ({
message, message,
senderProfile, senderProfile,
children, children,
onTap,
}: { }: {
message: ChatBskyConvoDefs.MessageView message: ChatBskyConvoDefs.MessageView
senderProfile?: bsky.profile.AnyProfileView senderProfile?: bsky.profile.AnyProfileView
children: TriggerProps['children'] children: TriggerProps['children']
onTap?: () => void
}): React.ReactNode => { }): React.ReactNode => {
const {t: l} = useLingui() const {t: l, i18n} = useLingui()
const ax = useAnalytics() const ax = useAnalytics()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -133,13 +131,15 @@ export let MessageContextMenu = ({
label={l`Message options`} label={l`Message options`}
contentLabel={l`Message from @${ contentLabel={l`Message from @${
sender?.handle ?? 'unknown' // should always be defined sender?.handle ?? 'unknown' // should always be defined
}: ${message.text}`} }: ${message.text}`}>
onTap={onTap}>
{children} {children}
</ContextMenu.Trigger> </ContextMenu.Trigger>
<ContextMenu.Outer <ContextMenu.Outer
align={isFromSelf ? 'right' : 'left'} align={isFromSelf ? 'right' : 'left'}
label={l`Sent at ${i18n.date(new Date(message.sentAt), {
timeStyle: 'short',
})}`}
style={[isFromSelf && isGroupChatEnabled ? null : a.ml_sm]}> style={[isFromSelf && isGroupChatEnabled ? null : a.ml_sm]}>
{message.text.length > 0 && ( {message.text.length > 0 && (
<> <>
@@ -147,35 +147,34 @@ export let MessageContextMenu = ({
testID="messageDropdownTranslateBtn" testID="messageDropdownTranslateBtn"
label={l`Translate`} label={l`Translate`}
onPress={onPressTranslateMessage}> onPress={onPressTranslateMessage}>
<ContextMenu.ItemIcon icon={LanguageIcon} position="left" />
<ContextMenu.ItemText>{l`Translate`}</ContextMenu.ItemText> <ContextMenu.ItemText>{l`Translate`}</ContextMenu.ItemText>
<ContextMenu.ItemIcon icon={TranslateIcon} position="right" />
</ContextMenu.Item> </ContextMenu.Item>
<ContextMenu.Item <ContextMenu.Item
testID="messageDropdownCopyBtn" testID="messageDropdownCopyBtn"
label={l`Copy message text`} label={l`Copy message text`}
onPress={onCopyMessage}> onPress={onCopyMessage}>
<ContextMenu.ItemIcon icon={ClipboardIcon} position="left" />
<ContextMenu.ItemText> <ContextMenu.ItemText>
{l`Copy message text`} {l`Copy message text`}
</ContextMenu.ItemText> </ContextMenu.ItemText>
<ContextMenu.ItemIcon icon={ClipboardIcon} position="right" />
</ContextMenu.Item> </ContextMenu.Item>
<ContextMenu.Divider />
</> </>
)} )}
<ContextMenu.Item <ContextMenu.Item
testID="messageDropdownDeleteBtn" testID="messageDropdownDeleteBtn"
label={l`Delete message for me`} label={l`Delete message for me`}
onPress={() => deleteControl.open()}> onPress={() => deleteControl.open()}>
<ContextMenu.ItemIcon icon={TrashIcon} position="left" />
<ContextMenu.ItemText>{l`Delete for me`}</ContextMenu.ItemText> <ContextMenu.ItemText>{l`Delete for me`}</ContextMenu.ItemText>
<ContextMenu.ItemIcon icon={TrashIcon} position="right" />
</ContextMenu.Item> </ContextMenu.Item>
{!isFromSelf && ( {!isFromSelf && (
<ContextMenu.Item <ContextMenu.Item
testID="messageDropdownReportBtn" testID="messageDropdownReportBtn"
label={l`Report message`} label={l`Report message`}
onPress={() => reportControl.open()}> onPress={() => reportControl.open()}>
<ContextMenu.ItemIcon icon={FlagIcon} position="left" />
<ContextMenu.ItemText>{l`Report`}</ContextMenu.ItemText> <ContextMenu.ItemText>{l`Report`}</ContextMenu.ItemText>
<ContextMenu.ItemIcon icon={WarningIcon} position="right" />
</ContextMenu.Item> </ContextMenu.Item>
)} )}
</ContextMenu.Outer> </ContextMenu.Outer>
+10 -41
View File
@@ -1,7 +1,6 @@
import {memo, useEffect, useMemo, useRef} from 'react' import {memo, useEffect, useMemo} from 'react'
import { import {
type GestureResponderEvent, type GestureResponderEvent,
LayoutAnimation,
Pressable, Pressable,
type StyleProp, type StyleProp,
type TextStyle, type TextStyle,
@@ -43,7 +42,6 @@ import * as ProfileCard from '#/components/ProfileCard'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {DateDivider} from './DateDivider' import {DateDivider} from './DateDivider'
import {useDateDividerToggle} from './DateDividerToggle'
import {MessageItemEmbed} from './MessageItemEmbed' import {MessageItemEmbed} from './MessageItemEmbed'
import {ReactionsDialog} from './ReactionsDialog' import {ReactionsDialog} from './ReactionsDialog'
@@ -56,8 +54,6 @@ const DISPLAY_NAME_INSET = 22
const CLUSTERED_MESSAGE_THRESHOLD_MS = 5 * 60 * 1000 const CLUSTERED_MESSAGE_THRESHOLD_MS = 5 * 60 * 1000
const MESSAGE_GAP_THRESHOLD_MS = 60 * 60 * 1000 const MESSAGE_GAP_THRESHOLD_MS = 60 * 60 * 1000
const TAP_AND_DRAG_DELAY_MS = 100
function isWithinClusterBoundary({ function isWithinClusterBoundary({
isPending, isPending,
adjacentMessage, adjacentMessage,
@@ -104,7 +100,6 @@ let MessageItem = ({
const profile = item.relatedProfiles.get(item.message.sender.did) const profile = item.relatedProfiles.get(item.message.sender.did)
const reactionsControl = useDialogControl() const reactionsControl = useDialogControl()
const reactionTapRef = useRef(false)
const {message, nextMessage, prevMessage} = item const {message, nextMessage, prevMessage} = item
const isPending = item.type === 'pending-message' const isPending = item.type === 'pending-message'
@@ -148,16 +143,9 @@ let MessageItem = ({
new Date(prevMessage.sentAt).getTime() > new Date(prevMessage.sentAt).getTime() >
MESSAGE_GAP_THRESHOLD_MS MESSAGE_GAP_THRESHOLD_MS
const {isDividerToggled, toggleDivider} = useDateDividerToggle() const isInCluster = !(isFirstInCluster && isLastInCluster)
const isDateDividerToggled = isDividerToggled(message.id)
const isNextDateDividerToggled =
nextMessage != null && isDividerToggled(nextMessage.id)
const effectiveFirstInCluster = isFirstInCluster || isDateDividerToggled
const effectiveLastInCluster = isLastInCluster || isNextDateDividerToggled
const isInCluster = !(effectiveFirstInCluster && effectiveLastInCluster)
const isInMiddleOfCluster = const isInMiddleOfCluster =
isInCluster && !effectiveFirstInCluster && !effectiveLastInCluster isInCluster && !isFirstInCluster && !isLastInCluster
const hasReactions = message.reactions && message.reactions.length > 0 const hasReactions = message.reactions && message.reactions.length > 0
const prevHasReactions = const prevHasReactions =
@@ -168,12 +156,12 @@ let MessageItem = ({
!hasReactions && !hasReactions &&
!isNextEmojiOnly && !isNextEmojiOnly &&
isInCluster && isInCluster &&
(isInMiddleOfCluster || effectiveFirstInCluster) (isInMiddleOfCluster || isFirstInCluster)
const squaredTopCorner = const squaredTopCorner =
!prevHasReactions && !prevHasReactions &&
!isPrevEmojiOnly && !isPrevEmojiOnly &&
isInCluster && isInCluster &&
(isInMiddleOfCluster || effectiveLastInCluster) (isInMiddleOfCluster || isLastInCluster)
const pendingColor = t.palette.primary_300 const pendingColor = t.palette.primary_300
@@ -336,16 +324,6 @@ let MessageItem = ({
transform: [{translateY: -8}], transform: [{translateY: -8}],
}, },
]} ]}
onPressIn={() => {
// Don't toggle the date divider when tapping a reaction.
reactionTapRef.current = true
}}
onPressOut={() => {
// Include a delay here to account for tap-and-drag before release.
setTimeout(() => {
reactionTapRef.current = false
}, TAP_AND_DRAG_DELAY_MS)
}}
onPress={isGroupChat ? reactionsControl.open : undefined}> onPress={isGroupChat ? reactionsControl.open : undefined}>
{groupedReactions.map(group => ( {groupedReactions.map(group => (
<Animated.View <Animated.View
@@ -407,13 +385,13 @@ let MessageItem = ({
return ( return (
<> <>
<LayoutAnimationConfig skipExiting skipEntering> <LayoutAnimationConfig skipExiting skipEntering>
{(hasLargeGapFromPrev || isDateDividerToggled) && ( {hasLargeGapFromPrev && (
<Animated.View entering={native(FadeIn)} exiting={native(FadeOut)}> <Animated.View entering={native(FadeIn)} exiting={native(FadeOut)}>
<DateDivider date={message.sentAt} /> <DateDivider date={message.sentAt} />
</Animated.View> </Animated.View>
)} )}
</LayoutAnimationConfig> </LayoutAnimationConfig>
<View style={[messageInset, effectiveFirstInCluster && a.mt_md]}> <View style={[messageInset, isFirstInCluster && a.mt_md]}>
<View style={[a.relative]}> <View style={[a.relative]}>
{showAvatar ? ( {showAvatar ? (
<View <View
@@ -450,16 +428,7 @@ let MessageItem = ({
hasReactions={hasReactions} hasReactions={hasReactions}
isFromSelf={isFromSelf} isFromSelf={isFromSelf}
message={message} message={message}
senderProfile={profile} senderProfile={profile}>
onTap={() => {
if (reactionTapRef.current) return
if (!hasLargeGapFromPrev) {
LayoutAnimation.configureNext(
LayoutAnimation.Presets.easeInEaseOut,
)
toggleDivider(message.id)
}
}}>
{AppBskyEmbedRecord.isView(message.embed) && ( {AppBskyEmbedRecord.isView(message.embed) && (
<MessageItemEmbed <MessageItemEmbed
embed={message.embed} embed={message.embed}
@@ -480,7 +449,7 @@ let MessageItem = ({
a.py_sm, a.py_sm,
a.px_md, a.px_md,
{ {
marginTop: effectiveFirstInCluster marginTop: isFirstInCluster
? 0 ? 0
: CLUSTERED_MESSAGE_GAP, : CLUSTERED_MESSAGE_GAP,
backgroundColor: isFromSelf backgroundColor: isFromSelf
@@ -523,7 +492,7 @@ let MessageItem = ({
</ActionsWrapper> </ActionsWrapper>
</View> </View>
</View> </View>
{effectiveLastInCluster && ( {isLastInCluster && (
<MessageItemMetadata <MessageItemMetadata
item={item} item={item}
style={[isFromSelf ? a.text_right : a.text_left]} style={[isFromSelf ? a.text_right : a.text_left]}
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Language_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M14.5 8a1 1 0 0 1 .912.59l4.5 10a1 1 0 0 1-1.824.82L16.554 16h-4.108l-1.534 3.41a1 1 0 0 1-1.824-.82l4.5-10A1 1 0 0 1 14.5 8m-1.153 6h2.306L14.5 11.437zM5.053.106a1 1 0 0 1 1.342.447L7.118 2H11a1 1 0 0 1 0 2H9.92C9.64 5.765 8.65 7.24 7.527 8.398a17 17 0 0 0 2.605 1.562q.129.062.197.092l.005.002.049.022.01.005a1 1 0 0 1-.787 1.838h-.002l-.027-.013-.068-.03q-.088-.04-.242-.113A18.878 18.878 0 0 1 6 9.75a19 19 0 0 1-3.509 2.125l-.068.03-.027.013h-.002a1 1 0 0 1-.788-1.838l.011-.005.049-.022.005-.002q.069-.03.197-.092a17.176 17.176 0 0 0 2.604-1.562C3.348 7.239 2.36 5.764 2.08 4H1a1 1 0 0 1 0-2h3.882l-.277-.553A1 1 0 0 1 5.053.106M4.118 4C4.388 5.151 5.098 6.193 6 7.1 6.901 6.193 7.612 5.15 7.882 4z',
})
@@ -55,7 +55,6 @@ import {MessageInput} from '#/screens/Messages/components/MessageInput'
import {MessageListError} from '#/screens/Messages/components/MessageListError' import {MessageListError} from '#/screens/Messages/components/MessageListError'
import {atoms as a, platform, tokens, useTheme, web} from '#/alf' import {atoms as a, platform, tokens, useTheme, web} from '#/alf'
import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill' import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill'
import {DateDividerToggleProvider} from '#/components/dms/DateDividerToggle'
import {MessageItem} from '#/components/dms/MessageItem' import {MessageItem} from '#/components/dms/MessageItem'
import {NewMessagesPill} from '#/components/dms/NewMessagesPill' import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
import {SystemMessageItem} from '#/components/dms/SystemMessageItem' import {SystemMessageItem} from '#/components/dms/SystemMessageItem'
@@ -410,7 +409,7 @@ export function MessagesList({
) )
return ( return (
<DateDividerToggleProvider> <>
<KeyboardGestureArea <KeyboardGestureArea
interpolator="ios" interpolator="ios"
// HACKFIX: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/1419 // HACKFIX: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/1419
@@ -514,7 +513,7 @@ export function MessagesList({
</KeyboardGestureArea> </KeyboardGestureArea>
{newMessagesPill.show && <NewMessagesPill onPress={scrollToEndOnPress} />} {newMessagesPill.show && <NewMessagesPill onPress={scrollToEndOnPress} />}
</DateDividerToggleProvider> </>
) )
} }