about summary refs log tree commit diff
path: root/src/view/com/util/MainScrollProvider.tsx
blob: 3ac28d31f0b8156dface66766569b062bd853f5a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import React, {useCallback} from 'react'
import {ScrollProvider} from '#/lib/ScrollContext'
import {NativeScrollEvent} from 'react-native'
import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
import {useShellLayout} from '#/state/shell/shell-layout'
import {isNative} from 'platform/detection'
import {useSharedValue, interpolate} from 'react-native-reanimated'

const WEB_HIDE_SHELL_THRESHOLD = 200

function clamp(num: number, min: number, max: number) {
  'worklet'
  return Math.min(Math.max(num, min), max)
}

export function MainScrollProvider({children}: {children: React.ReactNode}) {
  const {headerHeight} = useShellLayout()
  const mode = useMinimalShellMode()
  const setMode = useSetMinimalShellMode()
  const startDragOffset = useSharedValue<number | null>(null)
  const startMode = useSharedValue<number | null>(null)

  const onBeginDrag = useCallback(
    (e: NativeScrollEvent) => {
      'worklet'
      if (isNative) {
        startDragOffset.value = e.contentOffset.y
        startMode.value = mode.value
      }
    },
    [mode, startDragOffset, startMode],
  )

  const onEndDrag = useCallback(
    (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)
        }
      }
    },
    [startDragOffset, startMode, setMode, mode, headerHeight],
  )

  const onScroll = useCallback(
    (e: NativeScrollEvent) => {
      'worklet'
      if (isNative) {
        if (startDragOffset.value === null || startMode.value === null) {
          if (mode.value !== 0 && e.contentOffset.y < headerHeight.value) {
            // If we're close enough to the top, always show the shell.
            // Even if we're not dragging.
            setMode(false)
          }
          return
        }

        // The "mode" value is always between 0 and 1.
        // Figure out how much to move it based on the current dragged distance.
        const dy = e.contentOffset.y - startDragOffset.value
        const dProgress = interpolate(
          dy,
          [-headerHeight.value, headerHeight.value],
          [-1, 1],
        )
        const newValue = clamp(startMode.value + dProgress, 0, 1)
        if (newValue !== mode.value) {
          // Manually adjust the value. This won't be (and shouldn't be) animated.
          mode.value = newValue
        }
      } else {
        // On the web, we don't try to follow the drag because we don't know when it ends.
        // Instead, show/hide immediately based on whether we're scrolling up or down.
        const dy = e.contentOffset.y - (startDragOffset.value ?? 0)
        startDragOffset.value = e.contentOffset.y

        if (dy < 0 || e.contentOffset.y < WEB_HIDE_SHELL_THRESHOLD) {
          setMode(false)
        } else if (dy > 0) {
          setMode(true)
        }
      }
    },
    [headerHeight, mode, setMode, startDragOffset, startMode],
  )

  return (
    <ScrollProvider
      onBeginDrag={onBeginDrag}
      onEndDrag={onEndDrag}
      onScroll={onScroll}>
      {children}
    </ScrollProvider>
  )
}