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

Configure Feed

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

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