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