mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at remove-preload 182 lines 5.3 kB view raw
1import React, {useCallback, useEffect} from 'react' 2import {NativeScrollEvent} from 'react-native' 3import {interpolate, useSharedValue} from 'react-native-reanimated' 4import EventEmitter from 'eventemitter3' 5 6import {ScrollProvider} from '#/lib/ScrollContext' 7import {useMinimalShellMode, useSetMinimalShellMode} from '#/state/shell' 8import {useShellLayout} from '#/state/shell/shell-layout' 9import {isNative, isWeb} from 'platform/detection' 10 11const WEB_HIDE_SHELL_THRESHOLD = 200 12 13function clamp(num: number, min: number, max: number) { 14 'worklet' 15 return Math.min(Math.max(num, min), max) 16} 17 18export function MainScrollProvider({children}: {children: React.ReactNode}) { 19 const {headerHeight} = useShellLayout() 20 const mode = useMinimalShellMode() 21 const setMode = useSetMinimalShellMode() 22 const startDragOffset = useSharedValue<number | null>(null) 23 const startMode = useSharedValue<number | null>(null) 24 const didJustRestoreScroll = useSharedValue<boolean>(false) 25 26 useEffect(() => { 27 if (isWeb) { 28 return listenToForcedWindowScroll(() => { 29 startDragOffset.value = null 30 startMode.value = null 31 didJustRestoreScroll.value = true 32 }) 33 } 34 }) 35 36 const snapToClosestState = useCallback( 37 (e: NativeScrollEvent) => { 38 'worklet' 39 if (isNative) { 40 if (startDragOffset.value === null) { 41 return 42 } 43 const didScrollDown = e.contentOffset.y > startDragOffset.value 44 startDragOffset.value = null 45 startMode.value = null 46 if (e.contentOffset.y < headerHeight.value) { 47 // If we're close to the top, show the shell. 48 setMode(false) 49 } else if (didScrollDown) { 50 // Showing the bar again on scroll down feels annoying, so don't. 51 setMode(true) 52 } else { 53 // Snap to whichever state is the closest. 54 setMode(Math.round(mode.value) === 1) 55 } 56 } 57 }, 58 [startDragOffset, startMode, setMode, mode, headerHeight], 59 ) 60 61 const onBeginDrag = useCallback( 62 (e: NativeScrollEvent) => { 63 'worklet' 64 if (isNative) { 65 startDragOffset.value = e.contentOffset.y 66 startMode.value = mode.value 67 } 68 }, 69 [mode, startDragOffset, startMode], 70 ) 71 72 const onEndDrag = useCallback( 73 (e: NativeScrollEvent) => { 74 'worklet' 75 if (isNative) { 76 if (e.velocity && e.velocity.y !== 0) { 77 // If we detect a velocity, wait for onMomentumEnd to snap. 78 return 79 } 80 snapToClosestState(e) 81 } 82 }, 83 [snapToClosestState], 84 ) 85 86 const onMomentumEnd = useCallback( 87 (e: NativeScrollEvent) => { 88 'worklet' 89 if (isNative) { 90 snapToClosestState(e) 91 } 92 }, 93 [snapToClosestState], 94 ) 95 96 const onScroll = useCallback( 97 (e: NativeScrollEvent) => { 98 'worklet' 99 if (isNative) { 100 if (startDragOffset.value === null || startMode.value === null) { 101 if (mode.value !== 0 && e.contentOffset.y < headerHeight.value) { 102 // If we're close enough to the top, always show the shell. 103 // Even if we're not dragging. 104 setMode(false) 105 } 106 return 107 } 108 109 // The "mode" value is always between 0 and 1. 110 // Figure out how much to move it based on the current dragged distance. 111 const dy = e.contentOffset.y - startDragOffset.value 112 const dProgress = interpolate( 113 dy, 114 [-headerHeight.value, headerHeight.value], 115 [-1, 1], 116 ) 117 const newValue = clamp(startMode.value + dProgress, 0, 1) 118 if (newValue !== mode.value) { 119 // Manually adjust the value. This won't be (and shouldn't be) animated. 120 mode.value = newValue 121 } 122 } else { 123 if (didJustRestoreScroll.value) { 124 didJustRestoreScroll.value = false 125 // Don't hide/show navbar based on scroll restoratoin. 126 return 127 } 128 // On the web, we don't try to follow the drag because we don't know when it ends. 129 // Instead, show/hide immediately based on whether we're scrolling up or down. 130 const dy = e.contentOffset.y - (startDragOffset.value ?? 0) 131 startDragOffset.value = e.contentOffset.y 132 133 if (dy < 0 || e.contentOffset.y < WEB_HIDE_SHELL_THRESHOLD) { 134 setMode(false) 135 } else if (dy > 0) { 136 setMode(true) 137 } 138 } 139 }, 140 [ 141 headerHeight, 142 mode, 143 setMode, 144 startDragOffset, 145 startMode, 146 didJustRestoreScroll, 147 ], 148 ) 149 150 return ( 151 <ScrollProvider 152 onBeginDrag={onBeginDrag} 153 onEndDrag={onEndDrag} 154 onScroll={onScroll} 155 onMomentumEnd={onMomentumEnd}> 156 {children} 157 </ScrollProvider> 158 ) 159} 160 161const emitter = new EventEmitter() 162 163if (isWeb) { 164 const originalScroll = window.scroll 165 window.scroll = function () { 166 emitter.emit('forced-scroll') 167 return originalScroll.apply(this, arguments as any) 168 } 169 170 const originalScrollTo = window.scrollTo 171 window.scrollTo = function () { 172 emitter.emit('forced-scroll') 173 return originalScrollTo.apply(this, arguments as any) 174 } 175} 176 177function listenToForcedWindowScroll(listener: () => void) { 178 emitter.addListener('forced-scroll', listener) 179 return () => { 180 emitter.removeListener('forced-scroll', listener) 181 } 182}