this repo has no description
0
fork

Configure Feed

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

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