mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at thread-bug 358 lines 8.1 kB view raw
1import {cloneElement, Fragment, isValidElement, useMemo} from 'react' 2import { 3 Pressable, 4 type StyleProp, 5 type TextStyle, 6 View, 7 type ViewStyle, 8} from 'react-native' 9import {msg, Trans} from '@lingui/macro' 10import {useLingui} from '@lingui/react' 11import flattenReactChildren from 'react-keyed-flatten-children' 12 13import {isAndroid, isIOS, isNative} from '#/platform/detection' 14import {atoms as a, useTheme} from '#/alf' 15import {Button, ButtonText} from '#/components/Button' 16import * as Dialog from '#/components/Dialog' 17import {useInteractionState} from '#/components/hooks/useInteractionState' 18import { 19 Context, 20 ItemContext, 21 useMenuContext, 22 useMenuItemContext, 23} from '#/components/Menu/context' 24import { 25 type ContextType, 26 type GroupProps, 27 type ItemIconProps, 28 type ItemProps, 29 type ItemTextProps, 30 type TriggerProps, 31} from '#/components/Menu/types' 32import {Text} from '#/components/Typography' 33 34export { 35 type DialogControlProps as MenuControlProps, 36 useDialogControl as useMenuControl, 37} from '#/components/Dialog' 38 39export {useMenuContext} 40 41export function Root({ 42 children, 43 control, 44}: React.PropsWithChildren<{ 45 control?: Dialog.DialogControlProps 46}>) { 47 const defaultControl = Dialog.useDialogControl() 48 const context = useMemo<ContextType>( 49 () => ({ 50 control: control || defaultControl, 51 }), 52 [control, defaultControl], 53 ) 54 55 return <Context.Provider value={context}>{children}</Context.Provider> 56} 57 58export function Trigger({ 59 children, 60 label, 61 role = 'button', 62 hint, 63}: TriggerProps) { 64 const context = useMenuContext() 65 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 66 const { 67 state: pressed, 68 onIn: onPressIn, 69 onOut: onPressOut, 70 } = useInteractionState() 71 72 return children({ 73 isNative: true, 74 control: context.control, 75 state: { 76 hovered: false, 77 focused, 78 pressed, 79 }, 80 props: { 81 ref: null, 82 onPress: context.control.open, 83 onFocus, 84 onBlur, 85 onPressIn, 86 onPressOut, 87 accessibilityHint: hint, 88 accessibilityLabel: label, 89 accessibilityRole: role, 90 }, 91 }) 92} 93 94export function Outer({ 95 children, 96 showCancel, 97}: React.PropsWithChildren<{ 98 showCancel?: boolean 99 style?: StyleProp<ViewStyle> 100}>) { 101 const context = useMenuContext() 102 const {_} = useLingui() 103 104 return ( 105 <Dialog.Outer 106 control={context.control} 107 nativeOptions={{preventExpansion: true}}> 108 <Dialog.Handle /> 109 {/* Re-wrap with context since Dialogs are portal-ed to root */} 110 <Context.Provider value={context}> 111 <Dialog.ScrollableInner label={_(msg`Menu`)}> 112 <View style={[a.gap_lg]}> 113 {children} 114 {isNative && showCancel && <Cancel />} 115 </View> 116 </Dialog.ScrollableInner> 117 </Context.Provider> 118 </Dialog.Outer> 119 ) 120} 121 122export function Item({children, label, style, onPress, ...rest}: ItemProps) { 123 const t = useTheme() 124 const context = useMenuContext() 125 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 126 const { 127 state: pressed, 128 onIn: onPressIn, 129 onOut: onPressOut, 130 } = useInteractionState() 131 132 return ( 133 <Pressable 134 {...rest} 135 accessibilityHint="" 136 accessibilityLabel={label} 137 onFocus={onFocus} 138 onBlur={onBlur} 139 onPress={async e => { 140 if (isAndroid) { 141 /** 142 * Below fix for iOS doesn't work for Android, this does. 143 */ 144 onPress?.(e) 145 context.control.close() 146 } else if (isIOS) { 147 /** 148 * Fixes a subtle bug on iOS 149 * {@link https://github.com/bluesky-social/social-app/pull/5849/files#diff-de516ef5e7bd9840cd639213301df38cf03acfcad5bda85a1d63efd249ba79deL124-L127} 150 */ 151 context.control.close(() => { 152 onPress?.(e) 153 }) 154 } 155 }} 156 onPressIn={e => { 157 onPressIn() 158 rest.onPressIn?.(e) 159 }} 160 onPressOut={e => { 161 onPressOut() 162 rest.onPressOut?.(e) 163 }} 164 style={[ 165 a.flex_row, 166 a.align_center, 167 a.gap_sm, 168 a.px_md, 169 a.rounded_md, 170 a.border, 171 t.atoms.bg_contrast_25, 172 t.atoms.border_contrast_low, 173 {minHeight: 44, paddingVertical: 10}, 174 style, 175 (focused || pressed) && !rest.disabled && [t.atoms.bg_contrast_50], 176 ]}> 177 <ItemContext.Provider value={{disabled: Boolean(rest.disabled)}}> 178 {children} 179 </ItemContext.Provider> 180 </Pressable> 181 ) 182} 183 184export function ItemText({children, style}: ItemTextProps) { 185 const t = useTheme() 186 const {disabled} = useMenuItemContext() 187 return ( 188 <Text 189 numberOfLines={1} 190 ellipsizeMode="middle" 191 style={[ 192 a.flex_1, 193 a.text_md, 194 a.font_bold, 195 t.atoms.text_contrast_high, 196 {paddingTop: 3}, 197 style, 198 disabled && t.atoms.text_contrast_low, 199 ]}> 200 {children} 201 </Text> 202 ) 203} 204 205export function ItemIcon({icon: Comp}: ItemIconProps) { 206 const t = useTheme() 207 const {disabled} = useMenuItemContext() 208 return ( 209 <Comp 210 size="lg" 211 fill={ 212 disabled 213 ? t.atoms.text_contrast_low.color 214 : t.atoms.text_contrast_medium.color 215 } 216 /> 217 ) 218} 219 220export function ItemRadio({selected}: {selected: boolean}) { 221 const t = useTheme() 222 return ( 223 <View 224 style={[ 225 a.justify_center, 226 a.align_center, 227 a.rounded_full, 228 t.atoms.border_contrast_high, 229 { 230 borderWidth: 1, 231 height: 20, 232 width: 20, 233 }, 234 ]}> 235 {selected ? ( 236 <View 237 style={[ 238 a.absolute, 239 a.rounded_full, 240 {height: 14, width: 14}, 241 selected 242 ? { 243 backgroundColor: t.palette.primary_500, 244 } 245 : {}, 246 ]} 247 /> 248 ) : null} 249 </View> 250 ) 251} 252 253/** 254 * NATIVE ONLY - for adding non-pressable items to the menu 255 * 256 * @platform ios, android 257 */ 258export function ContainerItem({ 259 children, 260 style, 261}: { 262 children: React.ReactNode 263 style?: StyleProp<ViewStyle> 264}) { 265 const t = useTheme() 266 return ( 267 <View 268 style={[ 269 a.flex_row, 270 a.align_center, 271 a.gap_sm, 272 a.px_md, 273 a.rounded_md, 274 a.border, 275 t.atoms.bg_contrast_25, 276 t.atoms.border_contrast_low, 277 {paddingVertical: 10}, 278 style, 279 ]}> 280 {children} 281 </View> 282 ) 283} 284 285export function LabelText({ 286 children, 287 style, 288}: { 289 children: React.ReactNode 290 style?: StyleProp<TextStyle> 291}) { 292 const t = useTheme() 293 return ( 294 <Text 295 style={[ 296 a.font_bold, 297 t.atoms.text_contrast_medium, 298 {marginBottom: -8}, 299 style, 300 ]}> 301 {children} 302 </Text> 303 ) 304} 305 306export function Group({children, style}: GroupProps) { 307 const t = useTheme() 308 return ( 309 <View 310 style={[ 311 a.rounded_md, 312 a.overflow_hidden, 313 a.border, 314 t.atoms.border_contrast_low, 315 style, 316 ]}> 317 {flattenReactChildren(children).map((child, i) => { 318 return isValidElement(child) && 319 (child.type === Item || child.type === ContainerItem) ? ( 320 <Fragment key={i}> 321 {i > 0 ? ( 322 <View style={[a.border_b, t.atoms.border_contrast_low]} /> 323 ) : null} 324 {cloneElement(child, { 325 // @ts-expect-error cloneElement is not aware of the types 326 style: { 327 borderRadius: 0, 328 borderWidth: 0, 329 }, 330 })} 331 </Fragment> 332 ) : null 333 })} 334 </View> 335 ) 336} 337 338function Cancel() { 339 const {_} = useLingui() 340 const context = useMenuContext() 341 342 return ( 343 <Button 344 label={_(msg`Close this dialog`)} 345 size="small" 346 variant="ghost" 347 color="secondary" 348 onPress={() => context.control.close()}> 349 <ButtonText> 350 <Trans>Cancel</Trans> 351 </ButtonText> 352 </Button> 353 ) 354} 355 356export function Divider() { 357 return null 358}