forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React from 'react'
2import {Pressable, useWindowDimensions, View} from 'react-native'
3import Picker from '@emoji-mart/react'
4import {msg} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
6import {DismissableLayer, FocusScope} from 'radix-ui/internal'
7
8import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
9import {atoms as a, flatten} from '#/alf'
10import {Portal} from '#/components/Portal'
11
12const HEIGHT_OFFSET = 40
13const WIDTH_OFFSET = 100
14const PICKER_HEIGHT = 435 + HEIGHT_OFFSET
15const PICKER_WIDTH = 350 + WIDTH_OFFSET
16
17export type Emoji = {
18 aliases?: string[]
19 emoticons: string[]
20 id: string
21 keywords: string[]
22 name: string
23 native: string
24 shortcodes?: string
25 unified: string
26}
27
28export interface EmojiPickerPosition {
29 top: number
30 left: number
31 right: number
32 bottom: number
33 nextFocusRef: React.MutableRefObject<HTMLElement> | null
34}
35
36export interface EmojiPickerState {
37 isOpen: boolean
38 pos: EmojiPickerPosition
39}
40
41interface IProps {
42 state: EmojiPickerState
43 close: () => void
44 /**
45 * If `true`, overrides position and ensures picker is pinned to the top of
46 * the target element.
47 */
48 pinToTop?: boolean
49}
50
51export function EmojiPicker({state, close, pinToTop}: IProps) {
52 const {_} = useLingui()
53 const {height, width} = useWindowDimensions()
54
55 const isShiftDown = React.useRef(false)
56
57 const position = React.useMemo(() => {
58 if (pinToTop) {
59 return {
60 top: state.pos.top - PICKER_HEIGHT + HEIGHT_OFFSET - 10,
61 left: state.pos.left,
62 }
63 }
64
65 const fitsBelow = state.pos.top + PICKER_HEIGHT < height
66 const fitsAbove = PICKER_HEIGHT < state.pos.top
67 const placeOnLeft = PICKER_WIDTH < state.pos.left
68 const screenYMiddle = height / 2 - PICKER_HEIGHT / 2
69
70 if (fitsBelow) {
71 return {
72 top: state.pos.top + HEIGHT_OFFSET,
73 }
74 } else if (fitsAbove) {
75 return {
76 bottom: height - state.pos.bottom + HEIGHT_OFFSET,
77 }
78 } else {
79 return {
80 top: screenYMiddle,
81 left: placeOnLeft ? state.pos.left - PICKER_WIDTH : undefined,
82 right: !placeOnLeft
83 ? width - state.pos.right - PICKER_WIDTH
84 : undefined,
85 }
86 }
87 }, [state.pos, height, width, pinToTop])
88
89 React.useEffect(() => {
90 if (!state.isOpen) return
91
92 const onKeyDown = (e: KeyboardEvent) => {
93 if (e.key === 'Shift') {
94 isShiftDown.current = true
95 }
96 }
97 const onKeyUp = (e: KeyboardEvent) => {
98 if (e.key === 'Shift') {
99 isShiftDown.current = false
100 }
101 }
102 window.addEventListener('keydown', onKeyDown, true)
103 window.addEventListener('keyup', onKeyUp, true)
104
105 return () => {
106 window.removeEventListener('keydown', onKeyDown, true)
107 window.removeEventListener('keyup', onKeyUp, true)
108 }
109 }, [state.isOpen])
110
111 const onInsert = (emoji: Emoji) => {
112 textInputWebEmitter.emit('emoji-inserted', emoji)
113
114 if (!isShiftDown.current) {
115 close()
116 }
117 }
118
119 if (!state.isOpen) return null
120
121 return (
122 <Portal>
123 <FocusScope.FocusScope
124 loop
125 trapped
126 onUnmountAutoFocus={e => {
127 const nextFocusRef = state.pos.nextFocusRef
128 const node = nextFocusRef?.current
129 if (node) {
130 e.preventDefault()
131 node.focus()
132 }
133 }}>
134 <Pressable
135 accessible
136 accessibilityLabel={_(msg`Close emoji picker`)}
137 accessibilityHint={_(msg`Closes the emoji picker`)}
138 onPress={close}
139 style={[a.fixed, a.inset_0]}
140 />
141
142 <View
143 style={flatten([
144 a.fixed,
145 a.w_full,
146 a.h_full,
147 a.align_center,
148 a.z_10,
149 {
150 top: 0,
151 left: 0,
152 right: 0,
153 },
154 ])}>
155 <View style={[{position: 'absolute'}, position]}>
156 <DismissableLayer.DismissableLayer
157 onFocusOutside={evt => evt.preventDefault()}
158 onDismiss={close}>
159 <Picker
160 data={async () => {
161 return (await import('./EmojiPickerData.json')).default
162 }}
163 onEmojiSelect={onInsert}
164 autoFocus={true}
165 />
166 </DismissableLayer.DismissableLayer>
167 </View>
168 </View>
169
170 <Pressable
171 accessible
172 accessibilityLabel={_(msg`Close emoji picker`)}
173 accessibilityHint={_(msg`Closes the emoji picker`)}
174 onPress={close}
175 style={[a.fixed, a.inset_0]}
176 />
177 </FocusScope.FocusScope>
178 </Portal>
179 )
180}