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