Files
bsky-social-app/src/components/dms/EmojiReactionPicker.tsx
T
2026-04-21 09:24:46 -07:00

141 lines
4.3 KiB
TypeScript

import {useMemo, useState} from 'react'
import {useWindowDimensions, View} from 'react-native'
import {type ChatBskyConvoDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session'
import {atoms as a, tokens, useTheme} from '#/alf'
import * as ContextMenu from '#/components/ContextMenu'
import {
useContextMenuContext,
useContextMenuMenuContext,
} from '#/components/ContextMenu/context'
import {
EmojiHeartEyes_Stroke2_Corner0_Rounded as EmojiHeartEyesIcon,
EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon,
} from '#/components/icons/Emoji'
import {type TriggerProps} from '#/components/Menu/types'
import {Text} from '#/components/Typography'
import {EmojiPopup} from './EmojiPopup'
import {hasAlreadyReacted, hasReachedReactionLimit} from './util'
export function EmojiReactionPicker({
message,
onEmojiSelect,
}: {
message: ChatBskyConvoDefs.MessageView
children?: TriggerProps['children']
onEmojiSelect: (emoji: string) => void
}) {
const {_} = useLingui()
const {currentAccount} = useSession()
const t = useTheme()
const isFromSelf = message.sender?.did === currentAccount?.did
const {measurement, close} = useContextMenuContext()
const {align, xOffset} = useContextMenuMenuContext()
const [layout, setLayout] = useState({width: 0, height: 0})
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(() => {
return {
x:
align === 'left'
? (measurement?.x ?? 0) + xOffset
: (measurement?.x ?? 0) + (measurement?.width ?? 0) - layout.width,
y: (measurement?.y ?? 0) - tokens.space.xs - layout.height,
height: layout.height,
width: layout.width,
}
}, [measurement, align, xOffset, screenWidth, layout])
const limitReacted = hasReachedReactionLimit(message, currentAccount?.did)
const bgColor = t.scheme === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25
return (
<View
onLayout={evt => setLayout(evt.nativeEvent.layout)}
style={[
bgColor,
a.rounded_full,
a.absolute,
{bottom: '100%'},
isFromSelf ? a.right_0 : a.left_0,
a.flex_row,
a.p_xs,
a.gap_xs,
a.mb_xs,
a.z_20,
a.border,
t.atoms.border_contrast_low,
a.shadow_md,
]}>
{['👍', '😆', '❤️', '👀', '😢'].map(emoji => {
const alreadyReacted = hasAlreadyReacted(
message,
currentAccount?.did,
emoji,
)
return (
<ContextMenu.Item
position={position}
label={_(msg`React with ${emoji}`)}
key={emoji}
onPress={() => onEmojiSelect(emoji)}
unstyled
disabled={limitReacted ? !alreadyReacted : false}>
{hovered => (
<View
style={[
a.rounded_full,
hovered
? {
backgroundColor: alreadyReacted
? t.palette.negative_100
: t.palette.primary_500,
}
: alreadyReacted
? {backgroundColor: t.palette.primary_200}
: bgColor,
{height: 40, width: 40},
a.justify_center,
a.align_center,
]}>
<Text style={[a.text_center, {fontSize: 30}]} emoji>
{emoji}
</Text>
</View>
)}
</ContextMenu.Item>
)
})}
<EmojiPopup
onEmojiSelected={emoji => {
close()
onEmojiSelect(emoji)
}}>
<View
style={[
a.rounded_full,
t.scheme === 'light'
? t.atoms.bg_contrast_25
: t.atoms.bg_contrast_50,
{height: 40, width: 40},
a.justify_center,
a.align_center,
a.border,
t.atoms.border_contrast_low,
]}>
<EmojiIcon size="xl" fill={t.palette.contrast_400} />
</View>
</EmojiPopup>
</View>
)
}