fork
Configure Feed
Select the types of activity you want to include in your feed.
mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
fork
Configure Feed
Select the types of activity you want to include in your feed.
1import React, {PropsWithChildren, useMemo, useRef} from 'react'
2import {
3 Dimensions,
4 StyleProp,
5 StyleSheet,
6 TouchableOpacity,
7 TouchableWithoutFeedback,
8 View,
9 ViewStyle,
10} from 'react-native'
11import {IconProp} from '@fortawesome/fontawesome-svg-core'
12import RootSiblings from 'react-native-root-siblings'
13import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
14import {Text} from '../text/Text'
15import {Button, ButtonType} from './Button'
16import {colors} from 'lib/styles'
17import {usePalette} from 'lib/hooks/usePalette'
18import {useTheme} from 'lib/ThemeContext'
19import {HITSLOP_10} from 'lib/constants'
20import {useLingui} from '@lingui/react'
21import {msg} from '@lingui/macro'
22
23const ESTIMATED_BTN_HEIGHT = 50
24const ESTIMATED_SEP_HEIGHT = 16
25const ESTIMATED_HEADING_HEIGHT = 60
26
27export interface DropdownItemButton {
28 testID?: string
29 icon?: IconProp
30 label: string
31 onPress: () => void
32}
33export interface DropdownItemSeparator {
34 sep: true
35}
36export interface DropdownItemHeading {
37 heading: true
38 label: string
39}
40export type DropdownItem =
41 | DropdownItemButton
42 | DropdownItemSeparator
43 | DropdownItemHeading
44type MaybeDropdownItem = DropdownItem | false | undefined
45
46export type DropdownButtonType = ButtonType | 'bare'
47
48interface DropdownButtonProps {
49 testID?: string
50 type?: DropdownButtonType
51 style?: StyleProp<ViewStyle>
52 items: MaybeDropdownItem[]
53 label?: string
54 menuWidth?: number
55 children?: React.ReactNode
56 openToRight?: boolean
57 openUpwards?: boolean
58 rightOffset?: number
59 bottomOffset?: number
60 accessibilityLabel?: string
61 accessibilityHint?: string
62}
63
64export function DropdownButton({
65 testID,
66 type = 'bare',
67 style,
68 items,
69 label,
70 menuWidth,
71 children,
72 openToRight = false,
73 openUpwards = false,
74 rightOffset = 0,
75 bottomOffset = 0,
76 accessibilityLabel,
77}: PropsWithChildren<DropdownButtonProps>) {
78 const ref1 = useRef<TouchableOpacity>(null)
79 const ref2 = useRef<View>(null)
80
81 const onPress = () => {
82 const ref = ref1.current || ref2.current
83 ref?.measure(
84 (
85 _x: number,
86 _y: number,
87 width: number,
88 height: number,
89 pageX: number,
90 pageY: number,
91 ) => {
92 if (!menuWidth) {
93 menuWidth = 200
94 }
95 const winHeight = Dimensions.get('window').height
96 let estimatedMenuHeight = 0
97 for (const item of items) {
98 if (item && isSep(item)) {
99 estimatedMenuHeight += ESTIMATED_SEP_HEIGHT
100 } else if (item && isBtn(item)) {
101 estimatedMenuHeight += ESTIMATED_BTN_HEIGHT
102 } else if (item && isHeading(item)) {
103 estimatedMenuHeight += ESTIMATED_HEADING_HEIGHT
104 }
105 }
106 const newX = openToRight
107 ? pageX + width + rightOffset
108 : pageX + width - menuWidth
109 let newY = pageY + height + bottomOffset
110 if (openUpwards || newY + estimatedMenuHeight > winHeight) {
111 newY -= estimatedMenuHeight
112 }
113 createDropdownMenu(
114 newX,
115 newY,
116 menuWidth,
117 items.filter(v => !!v) as DropdownItem[],
118 )
119 },
120 )
121 }
122
123 const numItems = useMemo(
124 () =>
125 items.filter(item => {
126 if (item === undefined || item === false) {
127 return false
128 }
129
130 return isBtn(item)
131 }).length,
132 [items],
133 )
134
135 if (type === 'bare') {
136 return (
137 <TouchableOpacity
138 testID={testID}
139 style={style}
140 onPress={onPress}
141 hitSlop={HITSLOP_10}
142 ref={ref1}
143 accessibilityRole="button"
144 accessibilityLabel={accessibilityLabel || `Opens ${numItems} options`}
145 accessibilityHint="">
146 {children}
147 </TouchableOpacity>
148 )
149 }
150 return (
151 <View ref={ref2}>
152 <Button
153 type={type}
154 testID={testID}
155 onPress={onPress}
156 style={style}
157 label={label}>
158 {children}
159 </Button>
160 </View>
161 )
162}
163
164function createDropdownMenu(
165 x: number,
166 y: number,
167 width: number,
168 items: DropdownItem[],
169): RootSiblings {
170 const onPressItem = (index: number) => {
171 sibling.destroy()
172 const item = items[index]
173 if (isBtn(item)) {
174 item.onPress()
175 }
176 }
177 const onOuterPress = () => sibling.destroy()
178 const sibling = new RootSiblings(
179 (
180 <DropdownItems
181 onOuterPress={onOuterPress}
182 x={x}
183 y={y}
184 width={width}
185 items={items}
186 onPressItem={onPressItem}
187 />
188 ),
189 )
190 return sibling
191}
192
193type DropDownItemProps = {
194 onOuterPress: () => void
195 x: number
196 y: number
197 width: number
198 items: DropdownItem[]
199 onPressItem: (index: number) => void
200}
201
202const DropdownItems = ({
203 onOuterPress,
204 x,
205 y,
206 width,
207 items,
208 onPressItem,
209}: DropDownItemProps) => {
210 const pal = usePalette('default')
211 const theme = useTheme()
212 const {_} = useLingui()
213 const dropDownBackgroundColor =
214 theme.colorScheme === 'dark' ? pal.btn : pal.view
215 const separatorColor =
216 theme.colorScheme === 'dark' ? pal.borderDark : pal.border
217
218 const numItems = items.filter(isBtn).length
219
220 // TODO: Refactor dropdown components to:
221 // - (On web, if not handled by React Native) use semantic <select />
222 // and <option /> elements for keyboard navigation out of the box
223 // - (On mobile) be buttons by default, accept `label` and `nativeID`
224 // props, and always have an explicit label
225 return (
226 <>
227 {/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */}
228 <TouchableWithoutFeedback
229 onPress={onOuterPress}
230 accessibilityLabel={_(msg`Toggle dropdown`)}
231 accessibilityHint="">
232 <View style={[styles.bg]} />
233 </TouchableWithoutFeedback>
234 <View
235 style={[
236 styles.menu,
237 {left: x, top: y, width},
238 dropDownBackgroundColor,
239 ]}>
240 {items.map((item, index) => {
241 if (isBtn(item)) {
242 return (
243 <TouchableOpacity
244 testID={item.testID}
245 key={index}
246 style={[styles.menuItem]}
247 onPress={() => onPressItem(index)}
248 accessibilityRole="button"
249 accessibilityLabel={item.label}
250 accessibilityHint={`Option ${index + 1} of ${numItems}`}>
251 {item.icon && (
252 <FontAwesomeIcon
253 style={styles.icon}
254 icon={item.icon}
255 color={pal.text.color as string}
256 />
257 )}
258 <Text style={[styles.label, pal.text]}>{item.label}</Text>
259 </TouchableOpacity>
260 )
261 } else if (isSep(item)) {
262 return (
263 <View key={index} style={[styles.separator, separatorColor]} />
264 )
265 } else if (isHeading(item)) {
266 return (
267 <View style={[styles.heading, pal.border]} key={index}>
268 <Text style={[pal.text, styles.headingLabel]}>
269 {item.label}
270 </Text>
271 </View>
272 )
273 }
274 return null
275 })}
276 </View>
277 </>
278 )
279}
280
281function isSep(item: DropdownItem): item is DropdownItemSeparator {
282 return 'sep' in item && item.sep
283}
284function isHeading(item: DropdownItem): item is DropdownItemHeading {
285 return 'heading' in item && item.heading
286}
287function isBtn(item: DropdownItem): item is DropdownItemButton {
288 return !isSep(item) && !isHeading(item)
289}
290
291const styles = StyleSheet.create({
292 bg: {
293 position: 'absolute',
294 top: 0,
295 right: 0,
296 bottom: 0,
297 left: 0,
298 backgroundColor: '#000',
299 opacity: 0.1,
300 },
301 menu: {
302 position: 'absolute',
303 backgroundColor: '#fff',
304 borderRadius: 14,
305 opacity: 1,
306 paddingVertical: 6,
307 },
308 menuItem: {
309 flexDirection: 'row',
310 alignItems: 'center',
311 paddingVertical: 10,
312 paddingLeft: 15,
313 paddingRight: 40,
314 },
315 menuItemBorder: {
316 borderTopWidth: 1,
317 borderTopColor: colors.gray1,
318 marginTop: 4,
319 paddingTop: 12,
320 },
321 icon: {
322 marginLeft: 2,
323 marginRight: 8,
324 flexShrink: 0,
325 },
326 label: {
327 fontSize: 18,
328 flexShrink: 1,
329 flexGrow: 1,
330 },
331 separator: {
332 borderTopWidth: 1,
333 marginVertical: 8,
334 },
335 heading: {
336 flexDirection: 'row',
337 justifyContent: 'center',
338 paddingVertical: 10,
339 paddingLeft: 15,
340 paddingRight: 20,
341 borderBottomWidth: 1,
342 marginBottom: 6,
343 },
344 headingLabel: {
345 fontSize: 18,
346 fontWeight: '500',
347 },
348})