Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/nicer-tabs-2' into hailey/test-u…
Browse files Browse the repository at this point in the history
…pgrade-build
  • Loading branch information
haileyok committed Dec 2, 2024
2 parents 662ad55 + fc3ddd9 commit 98dd911
Show file tree
Hide file tree
Showing 9 changed files with 632 additions and 265 deletions.
6 changes: 0 additions & 6 deletions src/lib/statsig/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,6 @@ export type LogEvents = {
feedUrl: string
feedType: string
index: number
reason:
| 'focus'
| 'tabbar-click'
| 'pager-swipe'
| 'desktop-sidebar-click'
| 'starter-pack-initial-feed'
}
'feed:endReached': {
feedUrl: string
Expand Down
5 changes: 2 additions & 3 deletions src/view/com/home/HomeHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react'
import {useNavigation} from '@react-navigation/native'

import {usePalette} from '#/lib/hooks/usePalette'
import {NavigationProp} from '#/lib/routes/types'
import {FeedSourceInfo} from '#/state/queries/feed'
import {useSession} from '#/state/session'
Expand All @@ -19,7 +18,6 @@ export function HomeHeader(
const {feeds} = props
const {hasSession} = useSession()
const navigation = useNavigation<NavigationProp>()
const pal = usePalette('default')

const hasPinnedCustom = React.useMemo<boolean>(() => {
if (!hasSession) return false
Expand Down Expand Up @@ -61,7 +59,8 @@ export function HomeHeader(
onSelect={onSelect}
testID={props.testID}
items={items}
indicatorColor={pal.colors.link}
dragProgress={props.dragProgress}
dragState={props.dragState}
/>
</HomeHeaderLayout>
)
Expand Down
173 changes: 90 additions & 83 deletions src/view/com/pager/Pager.tsx
Original file line number Diff line number Diff line change
@@ -1,152 +1,159 @@
import React, {forwardRef} from 'react'
import {View} from 'react-native'
import PagerView, {
PagerViewOnPageScrollEvent,
PagerViewOnPageScrollEventData,
PagerViewOnPageSelectedEvent,
PageScrollStateChangedNativeEvent,
PagerViewOnPageSelectedEventData,
PageScrollStateChangedNativeEventData,
} from 'react-native-pager-view'
import Animated, {
runOnJS,
SharedValue,
useEvent,
useHandler,
useSharedValue,
} from 'react-native-reanimated'

import {LogEvents} from '#/lib/statsig/events'
import {atoms as a, native} from '#/alf'

export type PageSelectedEvent = PagerViewOnPageSelectedEvent

export interface PagerRef {
setPage: (
index: number,
reason: LogEvents['home:feedDisplayed']['reason'],
) => void
setPage: (index: number) => void
}

export interface RenderTabBarFnProps {
selectedPage: number
onSelect?: (index: number) => void
tabBarAnchor?: JSX.Element | null | undefined // Ignored on native.
dragProgress: SharedValue<number> // Ignored on web.
dragState: SharedValue<'idle' | 'dragging' | 'settling'> // Ignored on web.
}
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element

interface Props {
initialPage?: number
renderTabBar: RenderTabBarFn
onPageSelected?: (index: number) => void
onPageSelecting?: (
index: number,
reason: LogEvents['home:feedDisplayed']['reason'],
) => void
onPageScrollStateChanged?: (
scrollState: 'idle' | 'dragging' | 'settling',
) => void
testID?: string
}

const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)

export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
function PagerImpl(
{
children,
initialPage = 0,
renderTabBar,
onPageScrollStateChanged,
onPageSelected,
onPageSelecting,
onPageScrollStateChanged: parentOnPageScrollStateChanged,
onPageSelected: parentOnPageSelected,
testID,
}: React.PropsWithChildren<Props>,
ref,
) {
const [selectedPage, setSelectedPage] = React.useState(0)
const lastOffset = React.useRef(0)
const lastDirection = React.useRef(0)
const scrollState = React.useRef('')
const [selectedPage, setSelectedPage] = React.useState(initialPage)
const pagerView = React.useRef<PagerView>(null)

React.useImperativeHandle(ref, () => ({
setPage: (
index: number,
reason: LogEvents['home:feedDisplayed']['reason'],
) => {
setPage: (index: number) => {
pagerView.current?.setPage(index)
onPageSelecting?.(index, reason)
},
}))

const onPageSelectedInner = React.useCallback(
(e: PageSelectedEvent) => {
setSelectedPage(e.nativeEvent.position)
onPageSelected?.(e.nativeEvent.position)
},
[setSelectedPage, onPageSelected],
)

const onPageScroll = React.useCallback(
(e: PagerViewOnPageScrollEvent) => {
const {position, offset} = e.nativeEvent
if (offset === 0) {
// offset hits 0 in some awkward spots so we ignore it
return
}
// NOTE
// we want to call `onPageSelecting` as soon as the scroll-gesture
// enters the "settling" phase, which means the user has released it
// we can't infer directionality from the scroll information, so we
// track the offset changes. if the offset delta is consistent with
// the existing direction during the settling phase, we can say for
// certain where it's going and can fire
// -prf
if (scrollState.current === 'settling') {
if (lastDirection.current === -1 && offset < lastOffset.current) {
onPageSelecting?.(position, 'pager-swipe')
setSelectedPage(position)
lastDirection.current = 0
} else if (
lastDirection.current === 1 &&
offset > lastOffset.current
) {
onPageSelecting?.(position + 1, 'pager-swipe')
setSelectedPage(position + 1)
lastDirection.current = 0
}
} else {
if (offset < lastOffset.current) {
lastDirection.current = -1
} else if (offset > lastOffset.current) {
lastDirection.current = 1
}
}
lastOffset.current = offset
const onPageSelectedJSThread = React.useCallback(
(nextPosition: number) => {
setSelectedPage(nextPosition)
parentOnPageSelected?.(nextPosition)
},
[lastOffset, lastDirection, onPageSelecting],
)

const handlePageScrollStateChanged = React.useCallback(
(e: PageScrollStateChangedNativeEvent) => {
scrollState.current = e.nativeEvent.pageScrollState
onPageScrollStateChanged?.(e.nativeEvent.pageScrollState)
},
[scrollState, onPageScrollStateChanged],
[setSelectedPage, parentOnPageSelected],
)

const onTabBarSelect = React.useCallback(
(index: number) => {
pagerView.current?.setPage(index)
onPageSelecting?.(index, 'tabbar-click')
},
[pagerView, onPageSelecting],
[pagerView],
)

const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
const dragProgress = useSharedValue(selectedPage)
const handlePageScroll = usePagerHandlers(
{
onPageScroll(e: PagerViewOnPageScrollEventData) {
'worklet'
dragProgress.set(e.offset + e.position)
},
onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
'worklet'
if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
// This is a programmatic scroll on Android.
// Stay "idle" to match iOS and avoid confusing downstream code.
return
}
dragState.set(e.pageScrollState)
parentOnPageScrollStateChanged?.(e.pageScrollState)
},
onPageSelected(e: PagerViewOnPageSelectedEventData) {
'worklet'
runOnJS(onPageSelectedJSThread)(e.position)
},
},
[parentOnPageScrollStateChanged],
)

return (
<View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
{renderTabBar({
selectedPage,
onSelect: onTabBarSelect,
dragProgress,
dragState,
})}
<PagerView
<AnimatedPagerView
ref={pagerView}
style={[a.flex_1]}
initialPage={initialPage}
onPageScrollStateChanged={handlePageScrollStateChanged}
onPageSelected={onPageSelectedInner}
onPageScroll={onPageScroll}>
onPageScroll={handlePageScroll}>
{children}
</PagerView>
</AnimatedPagerView>
</View>
)
},
)

function usePagerHandlers(
handlers: {
onPageScroll: (e: PagerViewOnPageScrollEventData) => void
onPageScrollStateChanged: (e: PageScrollStateChangedNativeEventData) => void
onPageSelected: (e: PagerViewOnPageSelectedEventData) => void
},
dependencies: unknown[],
) {
const {doDependenciesDiffer} = useHandler(handlers as any, dependencies)
const subscribeForEvents = [
'onPageScroll',
'onPageScrollStateChanged',
'onPageSelected',
]
return useEvent(
event => {
'worklet'
const {onPageScroll, onPageScrollStateChanged, onPageSelected} = handlers
if (event.eventName.endsWith('onPageScroll')) {
onPageScroll(event as any as PagerViewOnPageScrollEventData)
} else if (event.eventName.endsWith('onPageScrollStateChanged')) {
onPageScrollStateChanged(
event as any as PageScrollStateChangedNativeEventData,
)
} else if (event.eventName.endsWith('onPageSelected')) {
onPageSelected(event as any as PagerViewOnPageSelectedEventData)
}
},
subscribeForEvents,
doDependenciesDiffer,
)
}
20 changes: 5 additions & 15 deletions src/view/com/pager/Pager.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react'
import {View} from 'react-native'
import {flushSync} from 'react-dom'

import {LogEvents} from '#/lib/statsig/events'
import {s} from '#/lib/styles'

export interface RenderTabBarFnProps {
Expand All @@ -16,18 +15,13 @@ interface Props {
initialPage?: number
renderTabBar: RenderTabBarFn
onPageSelected?: (index: number) => void
onPageSelecting?: (
index: number,
reason: LogEvents['home:feedDisplayed']['reason'],
) => void
}
export const Pager = React.forwardRef(function PagerImpl(
{
children,
initialPage = 0,
renderTabBar,
onPageSelected,
onPageSelecting,
}: React.PropsWithChildren<Props>,
ref,
) {
Expand All @@ -36,16 +30,13 @@ export const Pager = React.forwardRef(function PagerImpl(
const anchorRef = React.useRef(null)

React.useImperativeHandle(ref, () => ({
setPage: (
index: number,
reason: LogEvents['home:feedDisplayed']['reason'],
) => {
onTabBarSelect(index, reason)
setPage: (index: number) => {
onTabBarSelect(index)
},
}))

const onTabBarSelect = React.useCallback(
(index: number, reason: LogEvents['home:feedDisplayed']['reason']) => {
(index: number) => {
const scrollY = window.scrollY
// We want to determine if the tabbar is already "sticking" at the top (in which
// case we should preserve and restore scroll), or if it is somewhere below in the
Expand All @@ -64,7 +55,6 @@ export const Pager = React.forwardRef(function PagerImpl(
flushSync(() => {
setSelectedPage(index)
onPageSelected?.(index)
onPageSelecting?.(index, reason)
})
if (isSticking) {
const restoredScrollY = scrollYs.current[index]
Expand All @@ -75,15 +65,15 @@ export const Pager = React.forwardRef(function PagerImpl(
}
}
},
[selectedPage, setSelectedPage, onPageSelected, onPageSelecting],
[selectedPage, setSelectedPage, onPageSelected],
)

return (
<View style={s.hContentRegion}>
{renderTabBar({
selectedPage,
tabBarAnchor: <View ref={anchorRef} />,
onSelect: e => onTabBarSelect(e, 'tabbar-click'),
onSelect: e => onTabBarSelect(e),
})}
{React.Children.map(children, (child, i) => (
<View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
Expand Down
Loading

0 comments on commit 98dd911

Please sign in to comment.