mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

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

at sys-log 402 lines 12 kB view raw
1import React, {useImperativeHandle} from 'react' 2import { 3 type NativeScrollEvent, 4 type NativeSyntheticEvent, 5 Pressable, 6 type ScrollView, 7 type StyleProp, 8 TextInput, 9 View, 10 type ViewStyle, 11} from 'react-native' 12import { 13 KeyboardAwareScrollView, 14 useKeyboardHandler, 15 useReanimatedKeyboardAnimation, 16} from 'react-native-keyboard-controller' 17import Animated, { 18 runOnJS, 19 type ScrollEvent, 20 useAnimatedStyle, 21} from 'react-native-reanimated' 22import {useSafeAreaInsets} from 'react-native-safe-area-context' 23import {msg} from '@lingui/macro' 24import {useLingui} from '@lingui/react' 25 26import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController' 27import {ScrollProvider} from '#/lib/ScrollContext' 28import {logger} from '#/logger' 29import {isAndroid, isIOS} from '#/platform/detection' 30import {useA11y} from '#/state/a11y' 31import {useDialogStateControlContext} from '#/state/dialogs' 32import {List, type ListMethods, type ListProps} from '#/view/com/util/List' 33import {atoms as a, ios, platform, tokens, useTheme} from '#/alf' 34import {useThemeName} from '#/alf/util/useColorModeTheme' 35import {Context, useDialogContext} from '#/components/Dialog/context' 36import { 37 type DialogControlProps, 38 type DialogInnerProps, 39 type DialogOuterProps, 40} from '#/components/Dialog/types' 41import {createInput} from '#/components/forms/TextField' 42import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet' 43import { 44 type BottomSheetSnapPointChangeEvent, 45 type BottomSheetStateChangeEvent, 46} from '../../../modules/bottom-sheet/src/BottomSheet.types' 47import {type BottomSheetNativeComponent} from '../../../modules/bottom-sheet/src/BottomSheetNativeComponent' 48 49export {useDialogContext, useDialogControl} from '#/components/Dialog/context' 50export * from '#/components/Dialog/shared' 51export * from '#/components/Dialog/types' 52export * from '#/components/Dialog/utils' 53 54export const Input = createInput(TextInput) 55 56export function Outer({ 57 children, 58 control, 59 onClose, 60 nativeOptions, 61 testID, 62}: React.PropsWithChildren<DialogOuterProps>) { 63 const themeName = useThemeName() 64 const t = useTheme(themeName) 65 const ref = React.useRef<BottomSheetNativeComponent>(null) 66 const closeCallbacks = React.useRef<(() => void)[]>([]) 67 const {setDialogIsOpen, setFullyExpandedCount} = 68 useDialogStateControlContext() 69 70 const prevSnapPoint = React.useRef<BottomSheetSnapPoint>( 71 BottomSheetSnapPoint.Hidden, 72 ) 73 74 const [disableDrag, setDisableDrag] = React.useState(false) 75 const [snapPoint, setSnapPoint] = React.useState<BottomSheetSnapPoint>( 76 BottomSheetSnapPoint.Partial, 77 ) 78 79 const callQueuedCallbacks = React.useCallback(() => { 80 for (const cb of closeCallbacks.current) { 81 try { 82 cb() 83 } catch (e: any) { 84 logger.error(e || 'Error running close callback') 85 } 86 } 87 88 closeCallbacks.current = [] 89 }, []) 90 91 const open = React.useCallback<DialogControlProps['open']>(() => { 92 // Run any leftover callbacks that might have been queued up before calling `.open()` 93 callQueuedCallbacks() 94 setDialogIsOpen(control.id, true) 95 ref.current?.present() 96 }, [setDialogIsOpen, control.id, callQueuedCallbacks]) 97 98 // This is the function that we call when we want to dismiss the dialog. 99 const close = React.useCallback<DialogControlProps['close']>(cb => { 100 if (typeof cb === 'function') { 101 closeCallbacks.current.push(cb) 102 } 103 ref.current?.dismiss() 104 }, []) 105 106 // This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to 107 // happen before we run this. It is passed to the `BottomSheet` component. 108 const onCloseAnimationComplete = React.useCallback(() => { 109 // This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this 110 // tells us that we need to toggle the accessibility overlay setting 111 setDialogIsOpen(control.id, false) 112 callQueuedCallbacks() 113 onClose?.() 114 }, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen]) 115 116 const onSnapPointChange = (e: BottomSheetSnapPointChangeEvent) => { 117 const {snapPoint} = e.nativeEvent 118 setSnapPoint(snapPoint) 119 120 if ( 121 snapPoint === BottomSheetSnapPoint.Full && 122 prevSnapPoint.current !== BottomSheetSnapPoint.Full 123 ) { 124 setFullyExpandedCount(c => c + 1) 125 } else if ( 126 snapPoint !== BottomSheetSnapPoint.Full && 127 prevSnapPoint.current === BottomSheetSnapPoint.Full 128 ) { 129 setFullyExpandedCount(c => c - 1) 130 } 131 prevSnapPoint.current = snapPoint 132 } 133 134 const onStateChange = (e: BottomSheetStateChangeEvent) => { 135 if (e.nativeEvent.state === 'closed') { 136 onCloseAnimationComplete() 137 138 if (prevSnapPoint.current === BottomSheetSnapPoint.Full) { 139 setFullyExpandedCount(c => c - 1) 140 } 141 prevSnapPoint.current = BottomSheetSnapPoint.Hidden 142 } 143 } 144 145 useImperativeHandle( 146 control.ref, 147 () => ({ 148 open, 149 close, 150 }), 151 [open, close], 152 ) 153 154 const context = React.useMemo( 155 () => ({ 156 close, 157 isNativeDialog: true, 158 nativeSnapPoint: snapPoint, 159 disableDrag, 160 setDisableDrag, 161 isWithinDialog: true, 162 }), 163 [close, snapPoint, disableDrag, setDisableDrag], 164 ) 165 166 return ( 167 <BottomSheet 168 ref={ref} 169 cornerRadius={20} 170 backgroundColor={t.atoms.bg.backgroundColor} 171 {...nativeOptions} 172 onSnapPointChange={onSnapPointChange} 173 onStateChange={onStateChange} 174 disableDrag={disableDrag}> 175 <Context.Provider value={context}> 176 <View testID={testID} style={[a.relative]}> 177 {children} 178 </View> 179 </Context.Provider> 180 </BottomSheet> 181 ) 182} 183 184export function Inner({children, style, header}: DialogInnerProps) { 185 const insets = useSafeAreaInsets() 186 return ( 187 <> 188 {header} 189 <View 190 style={[ 191 a.pt_2xl, 192 a.px_xl, 193 { 194 paddingBottom: insets.bottom + insets.top, 195 }, 196 style, 197 ]}> 198 {children} 199 </View> 200 </> 201 ) 202} 203 204export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>( 205 function ScrollableInner( 206 {children, contentContainerStyle, header, ...props}, 207 ref, 208 ) { 209 const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() 210 const insets = useSafeAreaInsets() 211 212 useEnableKeyboardController(isIOS) 213 214 const [keyboardHeight, setKeyboardHeight] = React.useState(0) 215 216 useKeyboardHandler( 217 { 218 onEnd: e => { 219 'worklet' 220 runOnJS(setKeyboardHeight)(e.height) 221 }, 222 }, 223 [], 224 ) 225 226 let paddingBottom = 0 227 if (isIOS) { 228 paddingBottom += keyboardHeight / 4 229 if (nativeSnapPoint === BottomSheetSnapPoint.Full) { 230 paddingBottom += insets.bottom + tokens.space.md 231 } 232 paddingBottom = Math.max(paddingBottom, tokens.space._2xl) 233 } else { 234 paddingBottom += keyboardHeight 235 if (nativeSnapPoint === BottomSheetSnapPoint.Full) { 236 paddingBottom += insets.top 237 } 238 paddingBottom += 239 Math.max(insets.bottom, tokens.space._5xl) + tokens.space._2xl 240 } 241 242 const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => { 243 if (!isAndroid) { 244 return 245 } 246 const {contentOffset} = e.nativeEvent 247 if (contentOffset.y > 0 && !disableDrag) { 248 setDisableDrag(true) 249 } else if (contentOffset.y <= 1 && disableDrag) { 250 setDisableDrag(false) 251 } 252 } 253 254 return ( 255 <KeyboardAwareScrollView 256 contentContainerStyle={[ 257 a.pt_2xl, 258 a.px_xl, 259 {paddingBottom}, 260 contentContainerStyle, 261 ]} 262 ref={ref} 263 showsVerticalScrollIndicator={isAndroid ? false : undefined} 264 {...props} 265 bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} 266 bottomOffset={30} 267 scrollEventThrottle={50} 268 onScroll={isAndroid ? onScroll : undefined} 269 keyboardShouldPersistTaps="handled" 270 stickyHeaderIndices={header ? [0] : undefined}> 271 {header} 272 {children} 273 </KeyboardAwareScrollView> 274 ) 275 }, 276) 277 278export const InnerFlatList = React.forwardRef< 279 ListMethods, 280 ListProps<any> & { 281 webInnerStyle?: StyleProp<ViewStyle> 282 webInnerContentContainerStyle?: StyleProp<ViewStyle> 283 footer?: React.ReactNode 284 } 285>(function InnerFlatList({footer, style, ...props}, ref) { 286 const insets = useSafeAreaInsets() 287 const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() 288 289 useEnableKeyboardController(isIOS) 290 291 const onScroll = (e: ScrollEvent) => { 292 'worklet' 293 if (!isAndroid) { 294 return 295 } 296 const {contentOffset} = e 297 if (contentOffset.y > 0 && !disableDrag) { 298 runOnJS(setDisableDrag)(true) 299 } else if (contentOffset.y <= 1 && disableDrag) { 300 runOnJS(setDisableDrag)(false) 301 } 302 } 303 304 return ( 305 <ScrollProvider onScroll={onScroll}> 306 <List 307 keyboardShouldPersistTaps="handled" 308 bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} 309 ListFooterComponent={<View style={{height: insets.bottom + 100}} />} 310 ref={ref} 311 showsVerticalScrollIndicator={isAndroid ? false : undefined} 312 {...props} 313 style={[a.h_full, style]} 314 /> 315 {footer} 316 </ScrollProvider> 317 ) 318}) 319 320export function FlatListFooter({children}: {children: React.ReactNode}) { 321 const t = useTheme() 322 const {top, bottom} = useSafeAreaInsets() 323 const {height} = useReanimatedKeyboardAnimation() 324 325 const animatedStyle = useAnimatedStyle(() => { 326 if (!isIOS) return {} 327 return { 328 transform: [{translateY: Math.min(0, height.get() + bottom - 10)}], 329 } 330 }) 331 332 return ( 333 <Animated.View 334 style={[ 335 a.absolute, 336 a.bottom_0, 337 a.w_full, 338 a.z_10, 339 a.border_t, 340 t.atoms.bg, 341 t.atoms.border_contrast_low, 342 a.px_lg, 343 a.pt_md, 344 { 345 paddingBottom: platform({ 346 ios: tokens.space.md + bottom, 347 android: tokens.space.md + bottom + top, 348 }), 349 }, 350 // TODO: had to admit defeat here, but we should 351 // try and get this to work for Android as well -sfn 352 ios(animatedStyle), 353 ]}> 354 {children} 355 </Animated.View> 356 ) 357} 358 359export function Handle({difference = false}: {difference?: boolean}) { 360 const t = useTheme() 361 const {_} = useLingui() 362 const {screenReaderEnabled} = useA11y() 363 const {close} = useDialogContext() 364 365 return ( 366 <View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 20}]}> 367 <Pressable 368 accessible={screenReaderEnabled} 369 onPress={() => close()} 370 accessibilityLabel={_(msg`Dismiss`)} 371 accessibilityHint={_(msg`Double tap to close the dialog`)}> 372 <View 373 style={[ 374 a.rounded_sm, 375 { 376 top: tokens.space._2xl / 2 - 2.5, 377 width: 35, 378 height: 5, 379 alignSelf: 'center', 380 }, 381 difference 382 ? { 383 // TODO: mixBlendMode is only available on the new architecture -sfn 384 // backgroundColor: t.palette.white, 385 // mixBlendMode: 'difference', 386 backgroundColor: t.palette.white, 387 opacity: 0.75, 388 } 389 : { 390 backgroundColor: t.palette.contrast_975, 391 opacity: 0.5, 392 }, 393 ]} 394 /> 395 </Pressable> 396 </View> 397 ) 398} 399 400export function Close() { 401 return null 402}