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

Configure Feed

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

at cope-settings-sync 343 lines 9.0 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 isHeightConstrained: false, 116 }), 117 [close], 118 ) 119 120 return ( 121 <> 122 {isOpen && ( 123 <Portal> 124 <Context.Provider value={context}> 125 <RemoveScrollBar /> 126 <Pressable 127 accessibilityHint={undefined} 128 accessibilityLabel={_(msg`Close active dialog`)} 129 onPress={handleBackgroundPress}> 130 <View 131 style={[ 132 web(a.fixed), 133 a.inset_0, 134 a.z_10, 135 a.px_xl, 136 webOptions?.alignCenter ? a.justify_center : undefined, 137 a.align_center, 138 { 139 overflowY: 'auto', 140 paddingVertical: gtMobile ? '10vh' : a.pt_xl.paddingTop, 141 }, 142 ]}> 143 <Backdrop /> 144 {/** 145 * This is needed to prevent centered dialogs from overflowing 146 * above the screen, and provides a "natural" centering so that 147 * stacked dialogs appear relatively aligned. 148 */} 149 <View 150 style={[ 151 a.w_full, 152 a.z_20, 153 a.align_center, 154 web({minHeight: '60vh', position: 'static'}), 155 ]}> 156 {children} 157 </View> 158 </View> 159 </Pressable> 160 </Context.Provider> 161 </Portal> 162 )} 163 </> 164 ) 165} 166 167export function Inner({ 168 children, 169 style, 170 label, 171 accessibilityLabelledBy, 172 accessibilityDescribedBy, 173 header, 174 contentContainerStyle, 175}: DialogInnerProps) { 176 const t = useTheme() 177 const {close} = useContext(Context) 178 const {gtMobile} = useBreakpoints() 179 const {reduceMotionEnabled} = useA11y() 180 FocusGuards.useFocusGuards() 181 return ( 182 <FocusScope.FocusScope loop asChild trapped> 183 <View 184 role="dialog" 185 aria-role="dialog" 186 aria-label={label} 187 aria-labelledby={accessibilityLabelledBy} 188 aria-describedby={accessibilityDescribedBy} 189 // @ts-expect-error web only -prf 190 onClick={stopPropagation} 191 onStartShouldSetResponder={_ => true} 192 onTouchEnd={stopPropagation} 193 // note: flatten is required for some reason -sfn 194 style={flatten([ 195 a.relative, 196 a.rounded_md, 197 a.w_full, 198 a.border, 199 t.atoms.bg, 200 { 201 cursor: 'default', // The overlay applies `cursor: 'pointer'` to all children. 202 maxWidth: 600, 203 borderColor: t.palette.contrast_200, 204 shadowColor: t.palette.black, 205 shadowOpacity: t.name === 'light' ? 0.1 : 0.4, 206 shadowRadius: 30, 207 }, 208 !reduceMotionEnabled && a.zoom_fade_in, 209 style, 210 ])}> 211 <DismissableLayer.DismissableLayer 212 onInteractOutside={preventDefault} 213 onFocusOutside={preventDefault} 214 onDismiss={close} 215 style={{height: '100%', display: 'flex', flexDirection: 'column'}}> 216 {header} 217 <View style={[gtMobile ? a.p_2xl : a.p_xl, contentContainerStyle]}> 218 {children} 219 </View> 220 </DismissableLayer.DismissableLayer> 221 </View> 222 </FocusScope.FocusScope> 223 ) 224} 225 226export const ScrollableInner = Inner 227 228export const InnerFlatList = forwardRef< 229 FlatList, 230 FlatListProps<any> & {label: string} & { 231 webInnerStyle?: StyleProp<ViewStyle> 232 webInnerContentContainerStyle?: StyleProp<ViewStyle> 233 footer?: React.ReactNode 234 } 235>(function InnerFlatList( 236 { 237 label, 238 style, 239 webInnerStyle, 240 webInnerContentContainerStyle, 241 footer, 242 ...props 243 }, 244 ref, 245) { 246 const {gtMobile} = useBreakpoints() 247 return ( 248 <Inner 249 label={label} 250 style={[ 251 a.overflow_hidden, 252 a.px_0, 253 web({maxHeight: WEB_DIALOG_HEIGHT}), 254 webInnerStyle, 255 ]} 256 contentContainerStyle={[a.h_full, a.px_0, webInnerContentContainerStyle]}> 257 <FlatList 258 ref={ref} 259 style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, style]} 260 {...props} 261 /> 262 {footer} 263 </Inner> 264 ) 265}) 266 267export function FlatListFooter({ 268 children, 269 onLayout, 270}: { 271 children: React.ReactNode 272 onLayout?: (event: LayoutChangeEvent) => void 273}) { 274 const t = useTheme() 275 276 return ( 277 <View 278 onLayout={onLayout} 279 style={[ 280 a.absolute, 281 a.bottom_0, 282 a.w_full, 283 a.z_10, 284 t.atoms.bg, 285 a.border_t, 286 t.atoms.border_contrast_low, 287 a.px_lg, 288 a.py_md, 289 ]}> 290 {children} 291 </View> 292 ) 293} 294 295export function Close() { 296 const {_} = useLingui() 297 const {close} = useContext(Context) 298 299 const enableSquareButtons = useEnableSquareButtons() 300 301 return ( 302 <View 303 style={[ 304 a.absolute, 305 a.z_10, 306 { 307 top: a.pt_md.paddingTop, 308 right: a.pr_md.paddingRight, 309 }, 310 ]}> 311 <Button 312 size="small" 313 variant="ghost" 314 color="secondary" 315 shape={enableSquareButtons ? 'square' : 'round'} 316 onPress={() => close()} 317 label={_(msg`Close active dialog`)}> 318 <ButtonIcon icon={X} size="md" /> 319 </Button> 320 </View> 321 ) 322} 323 324export function Handle() { 325 return null 326} 327 328export function Backdrop() { 329 const t = useTheme() 330 const {reduceMotionEnabled} = useA11y() 331 return ( 332 <View style={{opacity: 0.8}}> 333 <View 334 style={[ 335 a.fixed, 336 a.inset_0, 337 {backgroundColor: t.palette.black}, 338 !reduceMotionEnabled && a.fade_in, 339 ]} 340 /> 341 </View> 342 ) 343}