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 uiwork 348 lines 8.6 kB view raw
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})