Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at 775a041b714c3ea5ae097f9df98c4480fee437a2 360 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, Trans} from '@lingui/macro' 10import {useLingui} from '@lingui/react' 11import flattenReactChildren from 'react-keyed-flatten-children' 12 13import {atoms as a, useTheme} from '#/alf' 14import {Button, ButtonText} from '#/components/Button' 15import * as Dialog from '#/components/Dialog' 16import {useInteractionState} from '#/components/hooks/useInteractionState' 17import { 18 Context, 19 ItemContext, 20 useMenuContext, 21 useMenuItemContext, 22} from '#/components/Menu/context' 23import { 24 type ContextType, 25 type GroupProps, 26 type ItemIconProps, 27 type ItemProps, 28 type ItemTextProps, 29 type TriggerProps, 30} from '#/components/Menu/types' 31import {Text} from '#/components/Typography' 32import {IS_ANDROID, IS_IOS, IS_NATIVE} from '#/env' 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 IS_NATIVE: 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 {IS_NATIVE && 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 (IS_ANDROID) { 141 /** 142 * Below fix for iOS doesn't work for Android, this does. 143 */ 144 onPress?.(e) 145 context.control.close() 146 } else if (IS_IOS) { 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.overflow_hidden, 171 a.border, 172 t.atoms.bg_contrast_25, 173 t.atoms.border_contrast_low, 174 {minHeight: 44, paddingVertical: 10}, 175 style, 176 (focused || pressed) && !rest.disabled && [t.atoms.bg_contrast_50], 177 ]}> 178 <ItemContext.Provider value={{disabled: Boolean(rest.disabled)}}> 179 {children} 180 </ItemContext.Provider> 181 </Pressable> 182 ) 183} 184 185export function ItemText({children, style}: ItemTextProps) { 186 const t = useTheme() 187 const {disabled} = useMenuItemContext() 188 return ( 189 <Text 190 numberOfLines={1} 191 ellipsizeMode="middle" 192 style={[ 193 a.flex_1, 194 a.text_md, 195 a.font_semi_bold, 196 t.atoms.text_contrast_high, 197 style, 198 disabled && t.atoms.text_contrast_low, 199 ]}> 200 {children} 201 </Text> 202 ) 203} 204 205export function ItemIcon({icon: Comp, fill}: ItemIconProps) { 206 const t = useTheme() 207 const {disabled} = useMenuItemContext() 208 return ( 209 <Comp 210 size="lg" 211 fill={ 212 fill 213 ? fill({disabled}) 214 : disabled 215 ? t.atoms.text_contrast_low.color 216 : t.atoms.text_contrast_medium.color 217 } 218 /> 219 ) 220} 221 222export function ItemRadio({selected}: {selected: boolean}) { 223 const t = useTheme() 224 return ( 225 <View 226 style={[ 227 a.justify_center, 228 a.align_center, 229 a.rounded_full, 230 t.atoms.border_contrast_high, 231 { 232 borderWidth: 1, 233 height: 20, 234 width: 20, 235 }, 236 ]}> 237 {selected ? ( 238 <View 239 style={[ 240 a.absolute, 241 a.rounded_full, 242 {height: 14, width: 14}, 243 selected 244 ? { 245 backgroundColor: t.palette.primary_500, 246 } 247 : {}, 248 ]} 249 /> 250 ) : null} 251 </View> 252 ) 253} 254 255/** 256 * NATIVE ONLY - for adding non-pressable items to the menu 257 * 258 * @platform ios, android 259 */ 260export function ContainerItem({ 261 children, 262 style, 263}: { 264 children: React.ReactNode 265 style?: StyleProp<ViewStyle> 266}) { 267 const t = useTheme() 268 return ( 269 <View 270 style={[ 271 a.flex_row, 272 a.align_center, 273 a.gap_sm, 274 a.px_md, 275 a.rounded_md, 276 a.border, 277 t.atoms.bg_contrast_25, 278 t.atoms.border_contrast_low, 279 {paddingVertical: 10}, 280 style, 281 ]}> 282 {children} 283 </View> 284 ) 285} 286 287export function LabelText({ 288 children, 289 style, 290}: { 291 children: React.ReactNode 292 style?: StyleProp<TextStyle> 293}) { 294 const t = useTheme() 295 return ( 296 <Text 297 style={[ 298 a.font_semi_bold, 299 t.atoms.text_contrast_medium, 300 {marginBottom: -8}, 301 style, 302 ]}> 303 {children} 304 </Text> 305 ) 306} 307 308export function Group({children, style}: GroupProps) { 309 const t = useTheme() 310 return ( 311 <View 312 style={[ 313 a.rounded_md, 314 a.overflow_hidden, 315 a.border, 316 t.atoms.border_contrast_low, 317 style, 318 ]}> 319 {flattenReactChildren(children).map((child, i) => { 320 return isValidElement(child) && 321 (child.type === Item || child.type === ContainerItem) ? ( 322 <Fragment key={i}> 323 {i > 0 ? ( 324 <View style={[a.border_b, t.atoms.border_contrast_low]} /> 325 ) : null} 326 {cloneElement(child, { 327 // @ts-expect-error cloneElement is not aware of the types 328 style: { 329 borderRadius: 0, 330 borderWidth: 0, 331 }, 332 })} 333 </Fragment> 334 ) : null 335 })} 336 </View> 337 ) 338} 339 340function Cancel() { 341 const {_} = useLingui() 342 const context = useMenuContext() 343 344 return ( 345 <Button 346 label={_(msg`Close this dialog`)} 347 size="small" 348 variant="ghost" 349 color="secondary" 350 onPress={() => context.control.close()}> 351 <ButtonText> 352 <Trans>Cancel</Trans> 353 </ButtonText> 354 </Button> 355 ) 356} 357 358export function Divider() { 359 return null 360}