Update context menu presentation (#10426)
This commit is contained in:
@@ -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 |
@@ -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,
|
||||||
]}>
|
]}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]}
|
||||||
|
|||||||
@@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user