✨ EmojiPicker component (#10249)

This commit is contained in:
Samuel Newman
2026-04-17 02:42:10 -07:00
committed by GitHub
parent 8f56fca82c
commit a97b15b204
17 changed files with 397 additions and 479 deletions
+30 -36
View File
@@ -72,7 +72,6 @@ import {
} from '#/lib/constants'
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {mimeToExt} from '#/lib/media/video/util'
import {useCallOnce} from '#/lib/once'
import {type NavigationProp} from '#/lib/routes/types'
@@ -122,9 +121,10 @@ import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog'
import {VideoPreview} from '#/view/com/composer/videos/VideoPreview'
import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, native, useTheme, web} from '#/alf'
import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as EmojiPicker from '#/components/EmojiPicker'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
@@ -185,7 +185,6 @@ export const ComposePost = ({
onPostSuccess,
quote: initQuote,
mention: initMention,
openEmojiPicker,
text: initText,
imageUris: initImageUris,
videoUri: initVideoUri,
@@ -206,7 +205,7 @@ export const ComposePost = ({
const requireAltTextEnabled = useRequireAltTextEnabled()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const textInput = useRef<TextInputRef>(null)
const textInputRef = useRef<TextInputRef>(null)
const discardPromptControl = Prompt.usePromptControl()
const {mutateAsync: saveDraft, isPending: _isSavingDraft} =
useSaveDraftMutation()
@@ -708,7 +707,7 @@ export const ComposePost = ({
)
const onPressCancel = useCallback(() => {
if (textInput.current?.maybeClosePopup()) {
if (textInputRef.current?.maybeClosePopup()) {
return
}
@@ -1064,17 +1063,6 @@ export const ComposePost = ({
}
}
const onEmojiButtonPress = useCallback(() => {
const rect = textInput.current?.getCursorPosition()
if (rect) {
openEmojiPicker?.({
...rect,
nextFocusRef:
textInput as unknown as React.MutableRefObject<HTMLElement>,
})
}
}, [openEmojiPicker])
const scrollViewRef = useAnimatedRef<Animated.ScrollView>()
useEffect(() => {
if (composerState.mutableNeedsFocusActive) {
@@ -1082,7 +1070,7 @@ export const ComposePost = ({
// On Android, this risks getting the cursor stuck behind the keyboard.
// Not worth it.
if (!IS_ANDROID) {
textInput.current?.focus()
textInputRef.current?.focus()
}
}
}, [composerState])
@@ -1123,7 +1111,6 @@ export const ComposePost = ({
!isEmptyPost(activePost) && (!nextPost || !isEmptyPost(nextPost))
}
onError={setError}
onEmojiButtonPress={onEmojiButtonPress}
onSelectVideo={selectVideo}
onAddPost={() => {
composerDispatch({
@@ -1133,6 +1120,7 @@ export const ComposePost = ({
currentLanguages={currentLanguages}
onSelectLanguage={onSelectLanguage}
openGallery={openGallery}
textInputRef={textInputRef}
/>
</>
)
@@ -1201,7 +1189,7 @@ export const ComposePost = ({
<ComposerPost
post={post}
dispatch={composerDispatch}
textInput={post.id === activePost.id ? textInput : null}
textInputRef={post.id === activePost.id ? textInputRef : null}
isFirstPost={index === 0}
isLastPost={index === thread.posts.length - 1}
isPartOfThread={thread.posts.length > 1}
@@ -1288,7 +1276,7 @@ export const ComposePost = ({
let ComposerPost = memo(function ComposerPost({
post,
dispatch,
textInput,
textInputRef,
isActive,
isReply,
isFirstPost,
@@ -1303,7 +1291,7 @@ let ComposerPost = memo(function ComposerPost({
}: {
post: PostDraft
dispatch: (action: ComposerAction) => void
textInput: React.Ref<TextInputRef>
textInputRef: React.RefObject<TextInputRef | null> | null
isActive: boolean
isReply: boolean
isFirstPost: boolean
@@ -1404,7 +1392,7 @@ let ComposerPost = memo(function ComposerPost({
style={[a.mt_xs]}
/>
<TextInput
ref={textInput}
ref={textInputRef}
style={[a.pt_xs]}
richtext={richtext}
placeholder={selectTextInputPlaceholder}
@@ -1822,27 +1810,27 @@ function ComposerFooter({
post,
dispatch,
showAddButton,
onEmojiButtonPress,
onSelectVideo,
onAddPost,
currentLanguages,
onSelectLanguage,
openGallery,
textInputRef,
}: {
post: PostDraft
dispatch: (action: PostAction) => void
showAddButton: boolean
onEmojiButtonPress: () => void
onError: (error: string) => void
onSelectVideo: (postId: string, asset: ImagePickerAsset) => void
onAddPost: () => void
currentLanguages: string[]
onSelectLanguage?: (language: string) => void
openGallery?: boolean
textInputRef: React.RefObject<TextInputRef | null>
}) {
const t = useTheme()
const {t: l} = useLingui()
const {isMobile} = useWebMediaQueries()
const {gtPhone} = useBreakpoints()
/*
* Once we've allowed a certain type of asset to be selected, we don't allow
* other types of media to be selected.
@@ -1965,17 +1953,23 @@ function ComposerFooter({
onAdd={onImageAdd}
/>
<SelectGifBtn onSelectGif={onSelectGif} disabled={!!media} />
{!isMobile ? (
<Button
onPress={onEmojiButtonPress}
style={a.p_sm}
label={l`Open emoji picker`}
accessibilityHint={l`Opens emoji picker`}
variant="ghost"
shape="round"
color="primary">
<EmojiSmileIcon size="lg" />
</Button>
{IS_WEB && gtPhone ? (
<EmojiPicker.Root nextFocusRef={textInputRef}>
<EmojiPicker.Trigger label={l`Open emoji picker`}>
{({props}) => (
<Button
style={a.p_sm}
label={props.accessibilityLabel}
variant="ghost"
shape="round"
color="primary"
{...props}>
<EmojiSmileIcon size="lg" />
</Button>
)}
</EmojiPicker.Trigger>
<EmojiPicker.Picker />
</EmojiPicker.Root>
) : null}
</ToolbarWrapper>
)}