diff --git a/src/state/shell/composer.tsx b/src/state/shell/composer.tsx
index bdf5e4a7..9cf8ef8d 100644
--- a/src/state/shell/composer.tsx
+++ b/src/state/shell/composer.tsx
@@ -30,6 +30,7 @@ export interface ComposerOpts {
onPost?: () => void
quote?: ComposerOptsQuote
mention?: string // handle of user to mention
+ openPicker?: (pos: DOMRect | undefined) => void
}
type StateContext = ComposerOpts | undefined
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 9f60923d..b15afe6f 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -6,6 +6,7 @@ import {
Keyboard,
KeyboardAvoidingView,
Platform,
+ Pressable,
ScrollView,
StyleSheet,
TouchableOpacity,
@@ -46,7 +47,6 @@ import {Gallery} from './photos/Gallery'
import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
import {LabelsBtn} from './labels/LabelsBtn'
import {SelectLangBtn} from './select-language/SelectLangBtn'
-import {EmojiPickerButton} from './text-input/web/EmojiPicker.web'
import {insertMentionAt} from 'lib/strings/mention-manip'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -70,6 +70,7 @@ export const ComposePost = observer(function ComposePost({
onPost,
quote: initQuote,
mention: initMention,
+ openPicker,
}: Props) {
const {currentAccount} = useSession()
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
@@ -274,6 +275,10 @@ export const ComposePost = observer(function ComposePost({
const canSelectImages = useMemo(() => gallery.size < 4, [gallery.size])
const hasMedia = gallery.size > 0 || Boolean(extLink)
+ const onEmojiButtonPress = useCallback(() => {
+ openPicker?.(textInput.current?.getCursorPosition())
+ }, [openPicker])
+
return (
>
) : null}
- {!isMobile ? : null}
+ {!isMobile ? (
+
+
+
+ ) : null}
diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx
index 7e39f6ae..57bfd0a8 100644
--- a/src/view/com/composer/text-input/TextInput.tsx
+++ b/src/view/com/composer/text-input/TextInput.tsx
@@ -32,6 +32,7 @@ import {POST_IMG_MAX} from 'lib/constants'
export interface TextInputRef {
focus: () => void
blur: () => void
+ getCursorPosition: () => DOMRect | undefined
}
interface TextInputProps extends ComponentProps {
@@ -74,6 +75,7 @@ export const TextInput = forwardRef(function TextInputImpl(
blur: () => {
textInput.current?.blur()
},
+ getCursorPosition: () => undefined, // Not implemented on native
}))
const onChangeText = useCallback(
diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index 206a3205..ec3a042a 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -22,6 +22,7 @@ import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
export interface TextInputRef {
focus: () => void
blur: () => void
+ getCursorPosition: () => DOMRect | undefined
}
interface TextInputProps {
@@ -169,6 +170,10 @@ export const TextInput = React.forwardRef(function TextInputImpl(
React.useImperativeHandle(ref, () => ({
focus: () => {}, // TODO
blur: () => {}, // TODO
+ getCursorPosition: () => {
+ const pos = editor?.state.selection.$anchor.pos
+ return pos ? editor?.view.coordsAtPos(pos) : undefined
+ },
}))
return (
diff --git a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx
index f4b2d99b..6d16403f 100644
--- a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx
+++ b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx
@@ -1,11 +1,17 @@
import React from 'react'
import Picker from '@emoji-mart/react'
-import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
+import {
+ StyleSheet,
+ TouchableWithoutFeedback,
+ useWindowDimensions,
+ View,
+} from 'react-native'
import {textInputWebEmitter} from '../TextInput.web'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useMediaQuery} from 'react-responsive'
+
+const HEIGHT_OFFSET = 40
+const WIDTH_OFFSET = 100
+const PICKER_HEIGHT = 435 + HEIGHT_OFFSET
+const PICKER_WIDTH = 350 + WIDTH_OFFSET
export type Emoji = {
aliases?: string[]
@@ -18,59 +24,87 @@ export type Emoji = {
unified: string
}
-export function EmojiPickerButton() {
- const pal = usePalette('default')
- const [open, setOpen] = React.useState(false)
- const onOpenChange = (o: boolean) => {
- setOpen(o)
- }
- const close = () => {
- setOpen(false)
- }
-
- return (
-
-
-
-
-
-
-
-
-
- )
+export interface EmojiPickerState {
+ isOpen: boolean
+ pos: {top: number; left: number; right: number; bottom: number}
}
-export function EmojiPicker({close}: {close: () => void}) {
+interface IProps {
+ state: EmojiPickerState
+ close: () => void
+}
+
+export function EmojiPicker({state, close}: IProps) {
+ const {height, width} = useWindowDimensions()
+
+ const isShiftDown = React.useRef(false)
+
+ const position = React.useMemo(() => {
+ const fitsBelow = state.pos.top + PICKER_HEIGHT < height
+ const fitsAbove = PICKER_HEIGHT < state.pos.top
+ const placeOnLeft = PICKER_WIDTH < state.pos.left
+ const screenYMiddle = height / 2 - PICKER_HEIGHT / 2
+
+ if (fitsBelow) {
+ return {
+ top: state.pos.top + HEIGHT_OFFSET,
+ }
+ } else if (fitsAbove) {
+ return {
+ bottom: height - state.pos.bottom + HEIGHT_OFFSET,
+ }
+ } else {
+ return {
+ top: screenYMiddle,
+ left: placeOnLeft ? state.pos.left - PICKER_WIDTH : undefined,
+ right: !placeOnLeft
+ ? width - state.pos.right - PICKER_WIDTH
+ : undefined,
+ }
+ }
+ }, [state.pos, height, width])
+
+ React.useEffect(() => {
+ if (!state.isOpen) return
+
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Shift') {
+ isShiftDown.current = true
+ }
+ }
+ const onKeyUp = (e: KeyboardEvent) => {
+ if (e.key === 'Shift') {
+ isShiftDown.current = false
+ }
+ }
+ window.addEventListener('keydown', onKeyDown, true)
+ window.addEventListener('keyup', onKeyUp, true)
+
+ return () => {
+ window.removeEventListener('keydown', onKeyDown, true)
+ window.removeEventListener('keyup', onKeyUp, true)
+ }
+ }, [state.isOpen])
+
const onInsert = (emoji: Emoji) => {
textInputWebEmitter.emit('emoji-inserted', emoji)
- close()
+
+ if (!isShiftDown.current) {
+ close()
+ }
}
- const reducedPadding = useMediaQuery({query: '(max-height: 750px)'})
- const noPadding = useMediaQuery({query: '(max-height: 550px)'})
- const noPicker = useMediaQuery({query: '(max-height: 350px)'})
+
+ if (!state.isOpen) return null
return (
- // eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors
-
+
{/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */}
- {
- e.stopPropagation() // prevent event from bubbling up to the mask
- }}>
-
+ e.stopPropagation()}>
+
{
return (await import('./EmojiPickerData.json')).default
@@ -93,15 +127,7 @@ const styles = StyleSheet.create({
right: 0,
width: '100%',
height: '100%',
- },
- trigger: {
- backgroundColor: 'transparent',
- // @ts-ignore web only -prf
- border: 'none',
- paddingTop: 4,
- paddingLeft: 12,
- paddingRight: 12,
- cursor: 'pointer',
+ alignItems: 'center',
},
picker: {
marginHorizontal: 'auto',
diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx
index 73f9f540..ed64bc79 100644
--- a/src/view/shell/Composer.web.tsx
+++ b/src/view/shell/Composer.web.tsx
@@ -5,6 +5,10 @@ import {ComposePost} from '../com/composer/Composer'
import {useComposerState} from 'state/shell/composer'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {
+ EmojiPicker,
+ EmojiPickerState,
+} from 'view/com/composer/text-input/web/EmojiPicker.web.tsx'
const BOTTOM_BAR_HEIGHT = 61
@@ -13,6 +17,26 @@ export function Composer({}: {winHeight: number}) {
const {isMobile} = useWebMediaQueries()
const state = useComposerState()
+ const [pickerState, setPickerState] = React.useState({
+ isOpen: false,
+ pos: {top: 0, left: 0, right: 0, bottom: 0},
+ })
+
+ const onOpenPicker = React.useCallback((pos: DOMRect | undefined) => {
+ if (!pos) return
+ setPickerState({
+ isOpen: true,
+ pos,
+ })
+ }, [])
+
+ const onClosePicker = React.useCallback(() => {
+ setPickerState(prev => ({
+ ...prev,
+ isOpen: false,
+ }))
+ }, [])
+
// rendering
// =
@@ -41,8 +65,10 @@ export function Composer({}: {winHeight: number}) {
quote={state.quote}
onPost={state.onPost}
mention={state.mention}
+ openPicker={onOpenPicker}
/>
+
)
}