mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {
2 type JSX,
3 memo,
4 useCallback,
5 useContext,
6 useImperativeHandle,
7 useMemo,
8 useRef,
9 useState,
10} from 'react'
11import {View} from 'react-native'
12import {DrawerGestureContext} from 'react-native-drawer-layout'
13import {Gesture, GestureDetector} from 'react-native-gesture-handler'
14import PagerView, {
15 type PagerViewOnPageScrollEventData,
16 type PagerViewOnPageSelectedEvent,
17 type PagerViewOnPageSelectedEventData,
18 type PageScrollStateChangedNativeEventData,
19} from 'react-native-pager-view'
20import Animated, {
21 runOnJS,
22 type SharedValue,
23 useEvent,
24 useHandler,
25 useSharedValue,
26} from 'react-native-reanimated'
27import {useFocusEffect} from '@react-navigation/native'
28
29import {useSetDrawerSwipeDisabled} from '#/state/shell'
30import {atoms as a, native} from '#/alf'
31
32export type PageSelectedEvent = PagerViewOnPageSelectedEvent
33
34export interface PagerRef {
35 setPage: (index: number) => void
36}
37
38export interface RenderTabBarFnProps {
39 selectedPage: number
40 onSelect?: (index: number) => void
41 tabBarAnchor?: JSX.Element | null | undefined // Ignored on native.
42 dragProgress: SharedValue<number> // Ignored on web.
43 dragState: SharedValue<'idle' | 'dragging' | 'settling'> // Ignored on web.
44}
45export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
46
47interface Props {
48 ref?: React.Ref<PagerRef>
49 initialPage?: number
50 renderTabBar: RenderTabBarFn
51 // tab pressed, yet to scroll to page
52 onTabPressed?: (index: number) => void
53 // scroll settled
54 onPageSelected?: (index: number) => void
55 onPageScrollStateChanged?: (
56 scrollState: 'idle' | 'dragging' | 'settling',
57 ) => void
58 testID?: string
59}
60
61const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
62const MemoizedAnimatedPagerView = memo(AnimatedPagerView)
63
64export function Pager({
65 ref,
66 children,
67 initialPage = 0,
68 renderTabBar,
69 onPageSelected: parentOnPageSelected,
70 onTabPressed: parentOnTabPressed,
71 onPageScrollStateChanged: parentOnPageScrollStateChanged,
72 testID,
73}: React.PropsWithChildren<Props>) {
74 const [selectedPage, setSelectedPage] = useState(initialPage)
75 const pagerView = useRef<PagerView>(null)
76
77 const [isIdle, setIsIdle] = useState(true)
78 const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
79 useFocusEffect(
80 useCallback(() => {
81 const canSwipeDrawer = selectedPage === 0 && isIdle
82 setDrawerSwipeDisabled(!canSwipeDrawer)
83 return () => {
84 setDrawerSwipeDisabled(false)
85 }
86 }, [setDrawerSwipeDisabled, selectedPage, isIdle]),
87 )
88
89 useImperativeHandle(ref, () => ({
90 setPage: (index: number) => {
91 pagerView.current?.setPage(index)
92 },
93 }))
94
95 const onPageSelectedJSThread = useCallback(
96 (nextPosition: number) => {
97 setSelectedPage(nextPosition)
98 parentOnPageSelected?.(nextPosition)
99 },
100 [setSelectedPage, parentOnPageSelected],
101 )
102
103 const onTabBarSelect = useCallback(
104 (index: number) => {
105 parentOnTabPressed?.(index)
106 pagerView.current?.setPage(index)
107 },
108 [pagerView, parentOnTabPressed],
109 )
110
111 const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
112 const dragProgress = useSharedValue(selectedPage)
113 const didInit = useSharedValue(false)
114 const handlePageScroll = usePagerHandlers(
115 {
116 onPageScroll(e: PagerViewOnPageScrollEventData) {
117 'worklet'
118 if (didInit.get() === false) {
119 // On iOS, there's a spurious scroll event with 0 position
120 // even if a different page was supplied as the initial page.
121 // Ignore it and wait for the first confirmed selection instead.
122 return
123 }
124 dragProgress.set(e.offset + e.position)
125 },
126 onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
127 'worklet'
128 runOnJS(setIsIdle)(e.pageScrollState === 'idle')
129 if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
130 // This is a programmatic scroll on Android.
131 // Stay "idle" to match iOS and avoid confusing downstream code.
132 return
133 }
134 dragState.set(e.pageScrollState)
135 parentOnPageScrollStateChanged?.(e.pageScrollState)
136 },
137 onPageSelected(e: PagerViewOnPageSelectedEventData) {
138 'worklet'
139 didInit.set(true)
140 runOnJS(onPageSelectedJSThread)(e.position)
141 },
142 },
143 [parentOnPageScrollStateChanged],
144 )
145
146 return (
147 <View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
148 {renderTabBar({
149 selectedPage,
150 onSelect: onTabBarSelect,
151 dragProgress,
152 dragState,
153 })}
154 <DrawerGestureRequireFail>
155 <MemoizedAnimatedPagerView
156 ref={pagerView}
157 style={a.flex_1}
158 initialPage={initialPage}
159 onPageScroll={handlePageScroll}>
160 {children}
161 </MemoizedAnimatedPagerView>
162 </DrawerGestureRequireFail>
163 </View>
164 )
165}
166
167function DrawerGestureRequireFail({children}: {children: React.ReactNode}) {
168 const drawerGesture = useContext(DrawerGestureContext)
169
170 const nativeGesture = useMemo(() => {
171 const gesture = Gesture.Native()
172 if (drawerGesture) {
173 gesture.requireExternalGestureToFail(drawerGesture)
174 }
175 return gesture
176 }, [drawerGesture])
177
178 return <GestureDetector gesture={nativeGesture}>{children}</GestureDetector>
179}
180
181function usePagerHandlers(
182 handlers: {
183 onPageScroll: (e: PagerViewOnPageScrollEventData) => void
184 onPageScrollStateChanged: (e: PageScrollStateChangedNativeEventData) => void
185 onPageSelected: (e: PagerViewOnPageSelectedEventData) => void
186 },
187 dependencies: unknown[],
188) {
189 const {doDependenciesDiffer} = useHandler(handlers as any, dependencies)
190 const subscribeForEvents = [
191 'onPageScroll',
192 'onPageScrollStateChanged',
193 'onPageSelected',
194 ]
195 return useEvent(
196 event => {
197 'worklet'
198 const {onPageScroll, onPageScrollStateChanged, onPageSelected} = handlers
199 if (event.eventName.endsWith('onPageScroll')) {
200 onPageScroll(event as any as PagerViewOnPageScrollEventData)
201 } else if (event.eventName.endsWith('onPageScrollStateChanged')) {
202 onPageScrollStateChanged(
203 event as any as PageScrollStateChangedNativeEventData,
204 )
205 } else if (event.eventName.endsWith('onPageSelected')) {
206 onPageSelected(event as any as PagerViewOnPageSelectedEventData)
207 }
208 },
209 subscribeForEvents,
210 doDependenciesDiffer,
211 )
212}