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.

at sonner 397 lines 10 kB view raw
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})