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

Configure Feed

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

at main 341 lines 8.8 kB view raw
1import { 2 forwardRef, 3 useCallback, 4 useContext, 5 useImperativeHandle, 6 useMemo, 7 useState, 8} from 'react' 9import { 10 FlatList, 11 type FlatListProps, 12 type GestureResponderEvent, 13 type LayoutChangeEvent, 14 Pressable, 15 type StyleProp, 16 View, 17 type ViewStyle, 18} from 'react-native' 19import {msg} from '@lingui/core/macro' 20import {useLingui} from '@lingui/react' 21import {DismissableLayer, FocusGuards, FocusScope} from 'radix-ui/internal' 22import {RemoveScrollBar} from 'react-remove-scroll-bar' 23 24import {logger} from '#/logger' 25import {useA11y} from '#/state/a11y' 26import {useDialogStateControlContext} from '#/state/dialogs' 27import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 28import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf' 29import {Button, ButtonIcon} from '#/components/Button' 30import {Context} from '#/components/Dialog/context' 31import { 32 type DialogControlProps, 33 type DialogInnerProps, 34 type DialogOuterProps, 35} from '#/components/Dialog/types' 36import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 37import {Portal} from '#/components/Portal' 38 39export {useDialogContext, useDialogControl} from '#/components/Dialog/context' 40export * from '#/components/Dialog/shared' 41export * from '#/components/Dialog/types' 42export * from '#/components/Dialog/utils' 43export {Input} from '#/components/forms/TextField' 44 45// 100 minus 10vh of paddingVertical 46export const WEB_DIALOG_HEIGHT = '80vh' 47 48const stopPropagation = (e: any) => e.stopPropagation() 49const preventDefault = (e: any) => e.preventDefault() 50 51export function Outer({ 52 children, 53 control, 54 onClose, 55 webOptions, 56}: React.PropsWithChildren<DialogOuterProps>) { 57 const {_} = useLingui() 58 const {gtMobile} = useBreakpoints() 59 const [isOpen, setIsOpen] = useState(false) 60 const {setDialogIsOpen} = useDialogStateControlContext() 61 62 const open = useCallback(() => { 63 setDialogIsOpen(control.id, true) 64 setIsOpen(true) 65 }, [setIsOpen, setDialogIsOpen, control.id]) 66 67 const close = useCallback<DialogControlProps['close']>( 68 cb => { 69 setDialogIsOpen(control.id, false) 70 setIsOpen(false) 71 72 try { 73 if (cb && typeof cb === 'function') { 74 // This timeout ensures that the callback runs at the same time as it would on native. I.e. 75 // console.log('Step 1') -> close(() => console.log('Step 3')) -> console.log('Step 2') 76 // This should always output 'Step 1', 'Step 2', 'Step 3', but without the timeout it would output 77 // 'Step 1', 'Step 3', 'Step 2'. 78 setTimeout(cb) 79 } 80 } catch (e: any) { 81 logger.error(`Dialog closeCallback failed`, { 82 message: e.message, 83 }) 84 } 85 86 onClose?.() 87 }, 88 [control.id, onClose, setDialogIsOpen], 89 ) 90 91 const handleBackgroundPress = useCallback( 92 async (e: GestureResponderEvent) => { 93 webOptions?.onBackgroundPress ? webOptions.onBackgroundPress(e) : close() 94 }, 95 [webOptions, close], 96 ) 97 98 useImperativeHandle( 99 control.ref, 100 () => ({ 101 open, 102 close, 103 }), 104 [close, open], 105 ) 106 107 const context = useMemo( 108 () => ({ 109 close, 110 isNativeDialog: false, 111 nativeSnapPoint: 0, 112 disableDrag: false, 113 setDisableDrag: () => {}, 114 isWithinDialog: true, 115 }), 116 [close], 117 ) 118 119 return ( 120 <> 121 {isOpen && ( 122 <Portal> 123 <Context.Provider value={context}> 124 <RemoveScrollBar /> 125 <Pressable 126 accessibilityHint={undefined} 127 accessibilityLabel={_(msg`Close active dialog`)} 128 onPress={handleBackgroundPress}> 129 <View 130 style={[ 131 web(a.fixed), 132 a.inset_0, 133 a.z_10, 134 a.px_xl, 135 webOptions?.alignCenter ? a.justify_center : undefined, 136 a.align_center, 137 { 138 overflowY: 'auto', 139 paddingVertical: gtMobile ? '10vh' : a.pt_xl.paddingTop, 140 }, 141 ]}> 142 <Backdrop /> 143 {/** 144 * This is needed to prevent centered dialogs from overflowing 145 * above the screen, and provides a "natural" centering so that 146 * stacked dialogs appear relatively aligned. 147 */} 148 <View 149 style={[ 150 a.w_full, 151 a.z_20, 152 a.align_center, 153 web({minHeight: '60vh', position: 'static'}), 154 ]}> 155 {children} 156 </View> 157 </View> 158 </Pressable> 159 </Context.Provider> 160 </Portal> 161 )} 162 </> 163 ) 164} 165 166export function Inner({ 167 children, 168 style, 169 label, 170 accessibilityLabelledBy, 171 accessibilityDescribedBy, 172 header, 173 contentContainerStyle, 174}: DialogInnerProps) { 175 const t = useTheme() 176 const {close} = useContext(Context) 177 const {gtMobile} = useBreakpoints() 178 const {reduceMotionEnabled} = useA11y() 179 FocusGuards.useFocusGuards() 180 return ( 181 <FocusScope.FocusScope loop asChild trapped> 182 <View 183 role="dialog" 184 aria-role="dialog" 185 aria-label={label} 186 aria-labelledby={accessibilityLabelledBy} 187 aria-describedby={accessibilityDescribedBy} 188 // @ts-expect-error web only -prf 189 onClick={stopPropagation} 190 onStartShouldSetResponder={_ => true} 191 onTouchEnd={stopPropagation} 192 // note: flatten is required for some reason -sfn 193 style={flatten([ 194 a.relative, 195 a.rounded_md, 196 a.w_full, 197 a.border, 198 t.atoms.bg, 199 { 200 maxWidth: 600, 201 borderColor: t.palette.contrast_200, 202 shadowColor: t.palette.black, 203 shadowOpacity: t.name === 'light' ? 0.1 : 0.4, 204 shadowRadius: 30, 205 }, 206 !reduceMotionEnabled && a.zoom_fade_in, 207 style, 208 ])}> 209 <DismissableLayer.DismissableLayer 210 onInteractOutside={preventDefault} 211 onFocusOutside={preventDefault} 212 onDismiss={close} 213 style={{height: '100%', display: 'flex', flexDirection: 'column'}}> 214 {header} 215 <View style={[gtMobile ? a.p_2xl : a.p_xl, contentContainerStyle]}> 216 {children} 217 </View> 218 </DismissableLayer.DismissableLayer> 219 </View> 220 </FocusScope.FocusScope> 221 ) 222} 223 224export const ScrollableInner = Inner 225 226export const InnerFlatList = forwardRef< 227 FlatList, 228 FlatListProps<any> & {label: string} & { 229 webInnerStyle?: StyleProp<ViewStyle> 230 webInnerContentContainerStyle?: StyleProp<ViewStyle> 231 footer?: React.ReactNode 232 } 233>(function InnerFlatList( 234 { 235 label, 236 style, 237 webInnerStyle, 238 webInnerContentContainerStyle, 239 footer, 240 ...props 241 }, 242 ref, 243) { 244 const {gtMobile} = useBreakpoints() 245 return ( 246 <Inner 247 label={label} 248 style={[ 249 a.overflow_hidden, 250 a.px_0, 251 web({maxHeight: WEB_DIALOG_HEIGHT}), 252 webInnerStyle, 253 ]} 254 contentContainerStyle={[a.h_full, a.px_0, webInnerContentContainerStyle]}> 255 <FlatList 256 ref={ref} 257 style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, style]} 258 {...props} 259 /> 260 {footer} 261 </Inner> 262 ) 263}) 264 265export function FlatListFooter({ 266 children, 267 onLayout, 268}: { 269 children: React.ReactNode 270 onLayout?: (event: LayoutChangeEvent) => void 271}) { 272 const t = useTheme() 273 274 return ( 275 <View 276 onLayout={onLayout} 277 style={[ 278 a.absolute, 279 a.bottom_0, 280 a.w_full, 281 a.z_10, 282 t.atoms.bg, 283 a.border_t, 284 t.atoms.border_contrast_low, 285 a.px_lg, 286 a.py_md, 287 ]}> 288 {children} 289 </View> 290 ) 291} 292 293export function Close() { 294 const {_} = useLingui() 295 const {close} = useContext(Context) 296 297 const enableSquareButtons = useEnableSquareButtons() 298 299 return ( 300 <View 301 style={[ 302 a.absolute, 303 a.z_10, 304 { 305 top: a.pt_md.paddingTop, 306 right: a.pr_md.paddingRight, 307 }, 308 ]}> 309 <Button 310 size="small" 311 variant="ghost" 312 color="secondary" 313 shape={enableSquareButtons ? 'square' : 'round'} 314 onPress={() => close()} 315 label={_(msg`Close active dialog`)}> 316 <ButtonIcon icon={X} size="md" /> 317 </Button> 318 </View> 319 ) 320} 321 322export function Handle() { 323 return null 324} 325 326export function Backdrop() { 327 const t = useTheme() 328 const {reduceMotionEnabled} = useA11y() 329 return ( 330 <View style={{opacity: 0.8}}> 331 <View 332 style={[ 333 a.fixed, 334 a.inset_0, 335 {backgroundColor: t.palette.black}, 336 !reduceMotionEnabled && a.fade_in, 337 ]} 338 /> 339 </View> 340 ) 341}