✨ EmojiPicker component (#10249)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {useState} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {
|
||||
useKeyboardHandler,
|
||||
@@ -25,14 +25,9 @@ import {
|
||||
useMessageDraft,
|
||||
useSaveMessageDraft,
|
||||
} from '#/state/messages/message-drafts'
|
||||
import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
|
||||
import {
|
||||
type Emoji,
|
||||
EmojiPicker,
|
||||
type EmojiPickerState,
|
||||
} from '#/view/com/composer/text-input/web/EmojiPicker'
|
||||
import {atoms as a, native, platform, tokens, useTheme, utils} from '#/alf'
|
||||
import {Composer, useComposerInternalApiRef} from '#/components/Composer'
|
||||
import * as EmojiPicker from '#/components/EmojiPicker'
|
||||
import {GlassView} from '#/components/GlassView'
|
||||
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
|
||||
import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
|
||||
@@ -60,10 +55,6 @@ export function MessageComposer({
|
||||
const {needsEmailVerification} = useEmail()
|
||||
const editable = !needsEmailVerification
|
||||
const {getDraft, clearDraft} = useMessageDraft()
|
||||
const [emojiPickerState, setEmojiPickerState] = useState<EmojiPickerState>({
|
||||
isOpen: false,
|
||||
pos: {top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null},
|
||||
})
|
||||
const composerInternalApiRef = useComposerInternalApiRef()
|
||||
|
||||
const [text, setText] = useState(getDraft)
|
||||
@@ -85,10 +76,6 @@ export function MessageComposer({
|
||||
|
||||
const submitDisabled = !editable || (!hasEmbed && text.trim().length === 0)
|
||||
|
||||
const openEmojiPicker = (pos: any) => {
|
||||
setEmojiPickerState({isOpen: true, pos})
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
if (!editable) return
|
||||
if (!hasEmbed && text.trim() === '') return
|
||||
@@ -112,16 +99,6 @@ export function MessageComposer({
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function onEmojiInserted(emoji: Emoji) {
|
||||
composerInternalApiRef.current?.insert(emoji.native)
|
||||
}
|
||||
textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted)
|
||||
return () => {
|
||||
textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted)
|
||||
}
|
||||
}, [composerInternalApiRef])
|
||||
|
||||
return (
|
||||
<ComposerContainer>
|
||||
{children}
|
||||
@@ -142,54 +119,47 @@ export function MessageComposer({
|
||||
tintColor={t.palette.contrast_50}
|
||||
fallbackStyle={[t.atoms.bg_contrast_50]}>
|
||||
{IS_WEB && (
|
||||
<Pressable
|
||||
onPress={e => {
|
||||
e.currentTarget.measure(
|
||||
(_fx, _fy, _width, _height, px, py) => {
|
||||
// TODO: rip this horrible system out
|
||||
openEmojiPicker?.({
|
||||
top: py,
|
||||
left: px - 400,
|
||||
right: px - 400,
|
||||
bottom: py,
|
||||
nextFocusRef: {
|
||||
current:
|
||||
composerInternalApiRef.current?.input?.element,
|
||||
<EmojiPicker.Root
|
||||
onEmojiSelect={emoji =>
|
||||
composerInternalApiRef.current?.insert(emoji.native)
|
||||
}
|
||||
nextFocusRef={() =>
|
||||
composerInternalApiRef.current?.input?.element
|
||||
}>
|
||||
<EmojiPicker.Trigger label={l`Open emoji picker`}>
|
||||
{({props, state, control}) => (
|
||||
<Pressable
|
||||
{...props}
|
||||
style={[
|
||||
a.overflow_hidden,
|
||||
a.absolute,
|
||||
a.rounded_full,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.z_30,
|
||||
{
|
||||
height: 20,
|
||||
width: 20,
|
||||
top: 10,
|
||||
right: 10,
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
}}
|
||||
style={[
|
||||
a.overflow_hidden,
|
||||
a.absolute,
|
||||
a.rounded_full,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.z_30,
|
||||
{
|
||||
height: 20,
|
||||
width: 20,
|
||||
top: 10,
|
||||
right: 10,
|
||||
},
|
||||
]}
|
||||
accessibilityLabel={l`Open emoji picker`}
|
||||
accessibilityHint="">
|
||||
{state => (
|
||||
<EmojiSmileIcon
|
||||
size="md"
|
||||
style={
|
||||
state.hovered ||
|
||||
state.focused ||
|
||||
state.pressed ||
|
||||
emojiPickerState.isOpen
|
||||
? {color: t.palette.primary_500}
|
||||
: t.atoms.text_contrast_high
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
]}>
|
||||
<EmojiSmileIcon
|
||||
size="md"
|
||||
style={
|
||||
state.hovered ||
|
||||
state.focused ||
|
||||
state.pressed ||
|
||||
control.isOpen
|
||||
? {color: t.palette.primary_500}
|
||||
: t.atoms.text_contrast_high
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</EmojiPicker.Trigger>
|
||||
<EmojiPicker.Picker />
|
||||
</EmojiPicker.Root>
|
||||
)}
|
||||
|
||||
<Composer
|
||||
@@ -226,14 +196,6 @@ export function MessageComposer({
|
||||
<SubmitButton onPress={onSubmit} disabled={submitDisabled} />
|
||||
</GlassContainer>
|
||||
</View>
|
||||
|
||||
{IS_WEB && (
|
||||
<EmojiPicker
|
||||
pinToTop
|
||||
state={emojiPickerState}
|
||||
close={() => setEmojiPickerState(prev => ({...prev, isOpen: false}))}
|
||||
/>
|
||||
)}
|
||||
</ComposerContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
useMessageDraft,
|
||||
useSaveMessageDraft,
|
||||
} from '#/state/messages/message-drafts'
|
||||
import {type EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker'
|
||||
import {atoms as a, platform, tokens, useTheme} from '#/alf'
|
||||
import {GlassView} from '#/components/GlassView'
|
||||
import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
|
||||
@@ -50,7 +49,6 @@ export function MessageInput({
|
||||
hasEmbed: boolean
|
||||
setEmbed: (embedUrl: string | undefined) => void
|
||||
children?: React.ReactNode
|
||||
openEmojiPicker?: (pos: EmojiPickerPosition) => void
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {useCallback, useRef, useState} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import {flushSync} from 'react-dom'
|
||||
@@ -11,13 +11,9 @@ import {
|
||||
useMessageDraft,
|
||||
useSaveMessageDraft,
|
||||
} from '#/state/messages/message-drafts'
|
||||
import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
|
||||
import {
|
||||
type Emoji,
|
||||
type EmojiPickerPosition,
|
||||
} from '#/view/com/composer/text-input/web/EmojiPicker'
|
||||
import {atoms as a, flatten, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import * as EmojiPicker from '#/components/EmojiPicker'
|
||||
import {useSharedInputStyles} from '#/components/forms/TextField'
|
||||
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
||||
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
|
||||
@@ -30,13 +26,11 @@ export function MessageInput({
|
||||
hasEmbed,
|
||||
setEmbed,
|
||||
children,
|
||||
openEmojiPicker,
|
||||
}: {
|
||||
onSendMessage: (message: string) => void
|
||||
hasEmbed: boolean
|
||||
setEmbed: (embedUrl: string | undefined) => void
|
||||
children?: React.ReactNode
|
||||
openEmojiPicker?: (pos: EmojiPickerPosition) => void
|
||||
}) {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const {t: l} = useLingui()
|
||||
@@ -104,12 +98,11 @@ export function MessageInput({
|
||||
}, [])
|
||||
|
||||
const onEmojiInserted = useCallback(
|
||||
(emoji: Emoji) => {
|
||||
(emoji: EmojiPicker.Emoji) => {
|
||||
if (!textAreaRef.current) {
|
||||
return
|
||||
}
|
||||
const position = textAreaRef.current.selectionStart ?? 0
|
||||
textAreaRef.current.focus()
|
||||
flushSync(() => {
|
||||
setMessage(
|
||||
message =>
|
||||
@@ -121,12 +114,6 @@ export function MessageInput({
|
||||
},
|
||||
[setMessage],
|
||||
)
|
||||
useEffect(() => {
|
||||
textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted)
|
||||
return () => {
|
||||
textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted)
|
||||
}
|
||||
}, [onEmojiInserted])
|
||||
|
||||
useSaveMessageDraft(message)
|
||||
useExtractEmbedFromFacets(message, setEmbed)
|
||||
@@ -152,49 +139,45 @@ export function MessageInput({
|
||||
// @ts-expect-error web only
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}>
|
||||
<Button
|
||||
onPress={e => {
|
||||
e.currentTarget.measure((_fx, _fy, _width, _height, px, py) => {
|
||||
openEmojiPicker?.({
|
||||
top: py,
|
||||
left: px,
|
||||
right: px,
|
||||
bottom: py,
|
||||
nextFocusRef:
|
||||
textAreaRef as unknown as React.MutableRefObject<HTMLElement>,
|
||||
})
|
||||
})
|
||||
}}
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.overflow_hidden,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{
|
||||
marginTop: 5,
|
||||
height: 30,
|
||||
width: 30,
|
||||
},
|
||||
]}
|
||||
label={l`Open emoji picker`}>
|
||||
{state => (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{
|
||||
backgroundColor:
|
||||
state.hovered || state.focused || state.pressed
|
||||
? t.atoms.bg.backgroundColor
|
||||
: undefined,
|
||||
},
|
||||
]}>
|
||||
<EmojiSmile size="lg" />
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
<EmojiPicker.Root
|
||||
onEmojiSelect={onEmojiInserted}
|
||||
nextFocusRef={textAreaRef}>
|
||||
<EmojiPicker.Trigger label={l`Open emoji picker`}>
|
||||
{({props, state}) => (
|
||||
<Button
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.overflow_hidden,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{
|
||||
marginTop: 5,
|
||||
height: 30,
|
||||
width: 30,
|
||||
},
|
||||
]}
|
||||
label={props.accessibilityLabel}
|
||||
{...props}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{
|
||||
backgroundColor:
|
||||
state.hovered || state.focused || state.pressed
|
||||
? t.atoms.bg.backgroundColor
|
||||
: undefined,
|
||||
},
|
||||
]}>
|
||||
<EmojiSmile size="lg" />
|
||||
</View>
|
||||
</Button>
|
||||
)}
|
||||
</EmojiPicker.Trigger>
|
||||
<EmojiPicker.Picker />
|
||||
</EmojiPicker.Root>
|
||||
<TextareaAutosize
|
||||
ref={textAreaRef}
|
||||
style={flatten([
|
||||
|
||||
@@ -49,10 +49,6 @@ import {
|
||||
} from '#/state/messages/convo/types'
|
||||
import {useGetPost} from '#/state/queries/post'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
EmojiPicker,
|
||||
type EmojiPickerState,
|
||||
} from '#/view/com/composer/text-input/web/EmojiPicker'
|
||||
import {List, type ListMethods} from '#/view/com/util/List'
|
||||
import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled'
|
||||
import {MessageComposer} from '#/screens/Messages/components/MessageComposer'
|
||||
@@ -124,11 +120,6 @@ export function MessagesList({
|
||||
startContentOffset: 0,
|
||||
})
|
||||
|
||||
const [emojiPickerState, setEmojiPickerState] = useState<EmojiPickerState>({
|
||||
isOpen: false,
|
||||
pos: {top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null},
|
||||
})
|
||||
|
||||
const inputHeightUI = useSharedValue(0)
|
||||
const [inputHeightJS, setInputHeightJS] = useState(0)
|
||||
|
||||
@@ -382,10 +373,6 @@ export function MessagesList({
|
||||
})
|
||||
}, [flatListRef])
|
||||
|
||||
const onOpenEmojiPicker = useCallback((pos: any) => {
|
||||
setEmojiPickerState({isOpen: true, pos})
|
||||
}, [])
|
||||
|
||||
const renderItem = ({item}: {item: ConvoItem}) => {
|
||||
if (item.type === 'message' || item.type === 'pending-message') {
|
||||
return (
|
||||
@@ -525,8 +512,7 @@ export function MessagesList({
|
||||
textInputId={textInputId}
|
||||
onSendMessage={onSendMessage}
|
||||
hasEmbed={!!embedUri}
|
||||
setEmbed={setEmbed}
|
||||
openEmojiPicker={onOpenEmojiPicker}>
|
||||
setEmbed={setEmbed}>
|
||||
<MessageInputEmbed embedUri={embedUri} setEmbed={setEmbed} />
|
||||
</MessageInput>
|
||||
)}
|
||||
@@ -535,14 +521,6 @@ export function MessagesList({
|
||||
</KeyboardStickyView>
|
||||
</KeyboardGestureArea>
|
||||
|
||||
{IS_WEB && (
|
||||
<EmojiPicker
|
||||
pinToTop
|
||||
state={emojiPickerState}
|
||||
close={() => setEmojiPickerState(prev => ({...prev, isOpen: false}))}
|
||||
/>
|
||||
)}
|
||||
|
||||
{newMessagesPill.show && <NewMessagesPill onPress={scrollToEndOnPress} />}
|
||||
</DateDividerToggleProvider>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user