Files
bsky-social-app/src/components/dms/EmojiReactionPicker.web.tsx
T
2026-05-22 23:07:47 +03:00

144 lines
4.2 KiB
TypeScript

import {useState} from 'react'
import {Pressable, View} from 'react-native'
import {type ChatBskyConvoDefs} from '@atproto/api'
import {useLingui} from '@lingui/react/macro'
import {DropdownMenu} from 'radix-ui'
import {useSession} from '#/state/session'
import {atoms as a, flatten, useTheme} from '#/alf'
import * as EmojiPicker from '#/components/EmojiPicker'
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import * as Menu from '#/components/Menu'
import {Text} from '#/components/Typography'
import {hasAlreadyReacted, hasReachedReactionLimit} from './util'
export function EmojiReactionPicker({
message,
children,
onEmojiSelect,
}: {
message: ChatBskyConvoDefs.MessageView
children?: EmojiPicker.TriggerProps['children']
onEmojiSelect: (emoji: string) => void
}) {
if (!children)
throw new Error('EmojiReactionPicker requires the children prop on web')
const {t: l} = useLingui()
return (
<EmojiPicker.Root onEmojiSelect={emoji => onEmojiSelect(emoji.native)}>
<EmojiPicker.Trigger label={l`Add emoji reaction`}>
{children}
</EmojiPicker.Trigger>
<MenuInner message={message} onEmojiSelect={onEmojiSelect} />
</EmojiPicker.Root>
)
}
function MenuInner({
message,
onEmojiSelect,
}: {
message: ChatBskyConvoDefs.MessageView
onEmojiSelect: (emoji: string) => void
}) {
const t = useTheme()
const {control} = Menu.useMenuContext()
const {currentAccount} = useSession()
const [expanded, setExpanded] = useState(false)
const [prevOpen, setPrevOpen] = useState(control.isOpen)
if (control.isOpen !== prevOpen) {
setPrevOpen(control.isOpen)
if (!control.isOpen) {
setExpanded(false)
}
}
const handleEmojiSelect = (emoji: string) => {
control.close()
onEmojiSelect(emoji)
}
const limitReacted = hasReachedReactionLimit(message, currentAccount?.did)
return expanded ? (
<EmojiPicker.Picker keepOpenWhenShiftHeld={false} />
) : (
<Menu.Outer
style={[a.rounded_full]}
onCloseAutoFocus={evt => {
// If something has already taken focus (e.g. emoji-mart's search
// input when swapping to the full picker), don't let Radix restore
// focus to the trigger and steal it back.
if (
document.activeElement &&
document.activeElement !== document.body
) {
evt.preventDefault()
}
}}>
<View style={[a.flex_row, a.gap_xs]}>
{['❤️', '👍', '😆', '👀', '😢'].map(emoji => {
const alreadyReacted = hasAlreadyReacted(
message,
currentAccount?.did,
emoji,
)
return (
<DropdownMenu.Item
key={emoji}
className={[
'EmojiReactionPicker__Pressable',
alreadyReacted && '__selected',
limitReacted && '__disabled',
]
.filter(Boolean)
.join(' ')}
onSelect={() => handleEmojiSelect(emoji)}
style={flatten([
a.flex,
a.flex_col,
a.rounded_full,
a.justify_center,
a.align_center,
a.transition_transform,
{
width: 34,
height: 34,
},
alreadyReacted && {
backgroundColor: t.atoms.bg_contrast_100.backgroundColor,
},
])}>
<Text style={[a.text_center, {fontSize: 28}]} emoji>
{emoji}
</Text>
</DropdownMenu.Item>
)
})}
<DropdownMenu.Item
asChild
className="EmojiReactionPicker__PickerButton">
<Pressable
accessibilityRole="button"
role="button"
onPress={() => setExpanded(true)}
style={flatten([
a.rounded_full,
{height: 34, width: 34},
t.atoms.bg_contrast_50,
a.justify_center,
a.align_center,
])}>
<PlusIcon size="md" style={t.atoms.text_contrast_medium} />
</Pressable>
</DropdownMenu.Item>
</View>
</Menu.Outer>
)
}