Bluesky app fork with some witchin' additions 馃挮
fork

Configure Feed

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

at main 412 lines 10 kB view raw
1import {forwardRef, useCallback, useId, useMemo, useState} from 'react' 2import { 3 Pressable, 4 type StyleProp, 5 type TextStyle, 6 View, 7 type ViewStyle, 8} from 'react-native' 9import {msg} from '@lingui/macro' 10import {useLingui} from '@lingui/react' 11import {DropdownMenu} from 'radix-ui' 12 13import {useA11y} from '#/state/a11y' 14import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 15import {atoms as a, flatten, useTheme, web} from '#/alf' 16import type * 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 RadixPassThroughTriggerProps, 31 type TriggerProps, 32} from '#/components/Menu/types' 33import {Portal} from '#/components/Portal' 34import {Text} from '#/components/Typography' 35 36export {useMenuContext} 37 38export function useMenuControl(): Dialog.DialogControlProps { 39 const id = useId() 40 const [isOpen, setIsOpen] = useState(false) 41 42 return useMemo( 43 () => ({ 44 id, 45 ref: {current: null}, 46 isOpen, 47 open() { 48 setIsOpen(true) 49 }, 50 close() { 51 setIsOpen(false) 52 }, 53 }), 54 [id, isOpen, setIsOpen], 55 ) 56} 57 58export function Root({ 59 children, 60 control, 61}: React.PropsWithChildren<{ 62 control?: Dialog.DialogControlProps 63}>) { 64 const {_} = useLingui() 65 const defaultControl = useMenuControl() 66 const context = useMemo<ContextType>( 67 () => ({ 68 control: control || defaultControl, 69 }), 70 [control, defaultControl], 71 ) 72 const onOpenChange = useCallback( 73 (open: boolean) => { 74 if (context.control.isOpen && !open) { 75 context.control.close() 76 } else if (!context.control.isOpen && open) { 77 context.control.open() 78 } 79 }, 80 [context.control], 81 ) 82 83 return ( 84 <Context.Provider value={context}> 85 {context.control.isOpen && ( 86 <Portal> 87 <Pressable 88 style={[a.fixed, a.inset_0, a.z_50]} 89 onPress={() => context.control.close()} 90 accessibilityHint="" 91 accessibilityLabel={_( 92 msg`Context menu backdrop, click to close the menu.`, 93 )} 94 /> 95 </Portal> 96 )} 97 <DropdownMenu.Root 98 open={context.control.isOpen} 99 onOpenChange={onOpenChange}> 100 {children} 101 </DropdownMenu.Root> 102 </Context.Provider> 103 ) 104} 105 106const RadixTriggerPassThrough = forwardRef( 107 ( 108 props: { 109 children: ( 110 props: RadixPassThroughTriggerProps & { 111 ref: React.Ref<any> 112 }, 113 ) => React.ReactNode 114 }, 115 ref, 116 ) => { 117 // @ts-expect-error Radix provides no types of this stuff 118 return props.children({...props, ref}) 119 }, 120) 121RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough' 122 123export function Trigger({ 124 children, 125 label, 126 role = 'button', 127 hint, 128}: TriggerProps) { 129 const {control} = useMenuContext() 130 const { 131 state: hovered, 132 onIn: onMouseEnter, 133 onOut: onMouseLeave, 134 } = useInteractionState() 135 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 136 137 return ( 138 <DropdownMenu.Trigger asChild> 139 <RadixTriggerPassThrough> 140 {props => 141 children({ 142 IS_NATIVE: false, 143 control, 144 state: { 145 hovered, 146 focused, 147 pressed: false, 148 }, 149 props: { 150 ...props, 151 // No-op override to prevent false positive that interprets mobile scroll as a tap. 152 // This requires the custom onPress handler below to compensate. 153 // https://github.com/radix-ui/primitives/issues/1912 154 onPointerDown: undefined, 155 onPress: () => { 156 if (window.event instanceof KeyboardEvent) { 157 // The onPointerDown hack above is not relevant to this press, so don't do anything. 158 return 159 } 160 // Compensate for the disabled onPointerDown above by triggering it manually. 161 if (control.isOpen) { 162 control.close() 163 } else { 164 control.open() 165 } 166 }, 167 onFocus: onFocus, 168 onBlur: onBlur, 169 onMouseEnter, 170 onMouseLeave, 171 accessibilityHint: hint, 172 accessibilityLabel: label, 173 accessibilityRole: role, 174 }, 175 }) 176 } 177 </RadixTriggerPassThrough> 178 </DropdownMenu.Trigger> 179 ) 180} 181 182export function Outer({ 183 children, 184 style, 185}: React.PropsWithChildren<{ 186 showCancel?: boolean 187 style?: StyleProp<ViewStyle> 188}>) { 189 const t = useTheme() 190 const {reduceMotionEnabled} = useA11y() 191 192 return ( 193 <DropdownMenu.Portal> 194 <DropdownMenu.Content 195 sideOffset={5} 196 collisionPadding={{left: 5, right: 5, bottom: 5}} 197 loop 198 aria-label="Test" 199 className="dropdown-menu-transform-origin dropdown-menu-constrain-size"> 200 <View 201 style={[ 202 a.rounded_sm, 203 a.p_xs, 204 a.border, 205 t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25, 206 t.atoms.shadow_md, 207 t.atoms.border_contrast_low, 208 a.overflow_auto, 209 !reduceMotionEnabled && a.zoom_fade_in, 210 style, 211 ]}> 212 {children} 213 </View> 214 215 {/* Disabled until we can fix positioning 216 <DropdownMenu.Arrow 217 className="DropdownMenuArrow" 218 fill={ 219 (t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25) 220 .backgroundColor 221 } 222 /> 223 */} 224 </DropdownMenu.Content> 225 </DropdownMenu.Portal> 226 ) 227} 228 229export function Item({children, label, onPress, style, ...rest}: ItemProps) { 230 const t = useTheme() 231 const {control} = useMenuContext() 232 const { 233 state: hovered, 234 onIn: onMouseEnter, 235 onOut: onMouseLeave, 236 } = useInteractionState() 237 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 238 239 return ( 240 <DropdownMenu.Item asChild> 241 <Pressable 242 {...rest} 243 className="radix-dropdown-item" 244 accessibilityHint="" 245 accessibilityLabel={label} 246 onPress={e => { 247 onPress(e) 248 249 /** 250 * Ported forward from Radix 251 * @see https://www.radix-ui.com/primitives/docs/components/dropdown-menu#item 252 */ 253 if (!e.defaultPrevented) { 254 control.close() 255 } 256 }} 257 onFocus={onFocus} 258 onBlur={onBlur} 259 // need `flatten` here for Radix compat 260 style={flatten([ 261 a.flex_row, 262 a.align_center, 263 a.gap_lg, 264 a.py_sm, 265 a.rounded_xs, 266 a.overflow_hidden, 267 {minHeight: 32, paddingHorizontal: 10}, 268 web({outline: 0}), 269 (hovered || focused) && 270 !rest.disabled && [ 271 web({outline: '0 !important'}), 272 t.name === 'light' 273 ? t.atoms.bg_contrast_25 274 : t.atoms.bg_contrast_50, 275 ], 276 style, 277 ])} 278 {...web({ 279 onMouseEnter, 280 onMouseLeave, 281 })}> 282 <ItemContext.Provider value={{disabled: Boolean(rest.disabled)}}> 283 {children} 284 </ItemContext.Provider> 285 </Pressable> 286 </DropdownMenu.Item> 287 ) 288} 289 290export function ItemText({children, style}: ItemTextProps) { 291 const t = useTheme() 292 const {disabled} = useMenuItemContext() 293 return ( 294 <Text 295 style={[ 296 a.flex_1, 297 a.font_semi_bold, 298 t.atoms.text_contrast_high, 299 style, 300 disabled && t.atoms.text_contrast_low, 301 ]}> 302 {children} 303 </Text> 304 ) 305} 306 307export function ItemIcon({icon: Comp, position = 'left', fill}: ItemIconProps) { 308 const t = useTheme() 309 const {disabled} = useMenuItemContext() 310 return ( 311 <View 312 style={[ 313 position === 'left' && { 314 marginLeft: -2, 315 }, 316 position === 'right' && { 317 marginRight: -2, 318 marginLeft: 12, 319 }, 320 ]}> 321 <Comp 322 size="md" 323 fill={ 324 fill 325 ? fill({disabled}) 326 : disabled 327 ? t.atoms.text_contrast_low.color 328 : t.atoms.text_contrast_medium.color 329 } 330 /> 331 </View> 332 ) 333} 334 335export function ItemRadio({selected}: {selected: boolean}) { 336 const t = useTheme() 337 const enableSquareButtons = useEnableSquareButtons() 338 return ( 339 <View 340 style={[ 341 a.justify_center, 342 a.align_center, 343 enableSquareButtons ? a.rounded_sm : a.rounded_full, 344 t.atoms.border_contrast_high, 345 { 346 borderWidth: 1, 347 height: 20, 348 width: 20, 349 }, 350 ]}> 351 {selected ? ( 352 <View 353 style={[ 354 a.absolute, 355 enableSquareButtons ? a.rounded_sm : a.rounded_full, 356 {height: 14, width: 14}, 357 selected 358 ? { 359 backgroundColor: t.palette.primary_500, 360 } 361 : {}, 362 ]} 363 /> 364 ) : null} 365 </View> 366 ) 367} 368 369export function LabelText({ 370 children, 371 style, 372}: { 373 children: React.ReactNode 374 style?: StyleProp<TextStyle> 375}) { 376 const t = useTheme() 377 return ( 378 <Text 379 style={[ 380 a.font_semi_bold, 381 a.p_sm, 382 t.atoms.text_contrast_low, 383 a.leading_snug, 384 {paddingHorizontal: 10}, 385 style, 386 ]}> 387 {children} 388 </Text> 389 ) 390} 391 392export function Group({children}: GroupProps) { 393 return children 394} 395 396export function Divider() { 397 const t = useTheme() 398 return ( 399 <DropdownMenu.Separator 400 style={flatten([ 401 a.my_xs, 402 t.atoms.bg_contrast_100, 403 a.flex_shrink_0, 404 {height: 1}, 405 ])} 406 /> 407 ) 408} 409 410export function ContainerItem() { 411 return null 412}