diff --git a/src/lib/ScrollContext.tsx b/src/lib/ScrollContext.tsx index 00b197be..d55b8cda 100644 --- a/src/lib/ScrollContext.tsx +++ b/src/lib/ScrollContext.tsx @@ -5,6 +5,7 @@ const ScrollContext = createContext>({ onBeginDrag: undefined, onEndDrag: undefined, onScroll: undefined, + onMomentumEnd: undefined, }) export function useScrollHandlers(): ScrollHandlers { @@ -20,14 +21,16 @@ export function ScrollProvider({ onBeginDrag, onEndDrag, onScroll, + onMomentumEnd, }: ProviderProps) { const handlers = useMemo( () => ({ onBeginDrag, onEndDrag, onScroll, + onMomentumEnd, }), - [onBeginDrag, onEndDrag, onScroll], + [onBeginDrag, onEndDrag, onScroll, onMomentumEnd], ) return ( {children} diff --git a/src/screens/Profile/Sections/Labels.tsx b/src/screens/Profile/Sections/Labels.tsx index f43e3633..553d94d2 100644 --- a/src/screens/Profile/Sections/Labels.tsx +++ b/src/screens/Profile/Sections/Labels.tsx @@ -123,6 +123,9 @@ export function ProfileLabelsSectionInner({ onScroll(e, ctx) { contextScrollHandlers.onScroll?.(e, ctx) }, + onMomentumEnd(e, ctx) { + contextScrollHandlers.onMomentumEnd?.(e, ctx) + }, }) const {labelValues} = labelerInfo.policies diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index 5729a43a..d96a634e 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -64,6 +64,11 @@ function ListImpl( } } }, + // Note: adding onMomentumBegin here makes simulator scroll + // lag on Android. So either don't add it, or figure out why. + onMomentumEnd(e, ctx) { + contextScrollHandlers.onMomentumEnd?.(e, ctx) + }, }) let refreshControl diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx index 01b8a954..f45229dc 100644 --- a/src/view/com/util/MainScrollProvider.tsx +++ b/src/view/com/util/MainScrollProvider.tsx @@ -1,11 +1,12 @@ import React, {useCallback, useEffect} from 'react' -import EventEmitter from 'eventemitter3' -import {ScrollProvider} from '#/lib/ScrollContext' import {NativeScrollEvent} from 'react-native' -import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell' +import {interpolate, useSharedValue} from 'react-native-reanimated' +import EventEmitter from 'eventemitter3' + +import {ScrollProvider} from '#/lib/ScrollContext' +import {useMinimalShellMode, useSetMinimalShellMode} from '#/state/shell' import {useShellLayout} from '#/state/shell/shell-layout' import {isNative, isWeb} from 'platform/detection' -import {useSharedValue, interpolate} from 'react-native-reanimated' const WEB_HIDE_SHELL_THRESHOLD = 200 @@ -32,6 +33,31 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { } }) + const snapToClosestState = useCallback( + (e: NativeScrollEvent) => { + 'worklet' + if (isNative) { + if (startDragOffset.value === null) { + return + } + const didScrollDown = e.contentOffset.y > startDragOffset.value + startDragOffset.value = null + startMode.value = null + if (e.contentOffset.y < headerHeight.value) { + // If we're close to the top, show the shell. + setMode(false) + } else if (didScrollDown) { + // Showing the bar again on scroll down feels annoying, so don't. + setMode(true) + } else { + // Snap to whichever state is the closest. + setMode(Math.round(mode.value) === 1) + } + } + }, + [startDragOffset, startMode, setMode, mode, headerHeight], + ) + const onBeginDrag = useCallback( (e: NativeScrollEvent) => { 'worklet' @@ -47,18 +73,24 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { (e: NativeScrollEvent) => { 'worklet' if (isNative) { - startDragOffset.value = null - startMode.value = null - if (e.contentOffset.y < headerHeight.value / 2) { - // If we're close to the top, show the shell. - setMode(false) - } else { - // Snap to whichever state is the closest. - setMode(Math.round(mode.value) === 1) + if (e.velocity && e.velocity.y !== 0) { + // If we detect a velocity, wait for onMomentumEnd to snap. + return } + snapToClosestState(e) } }, - [startDragOffset, startMode, setMode, mode, headerHeight], + [snapToClosestState], + ) + + const onMomentumEnd = useCallback( + (e: NativeScrollEvent) => { + 'worklet' + if (isNative) { + snapToClosestState(e) + } + }, + [snapToClosestState], ) const onScroll = useCallback( @@ -119,7 +151,8 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { + onScroll={onScroll} + onMomentumEnd={onMomentumEnd}> {children} )