diff options
Diffstat (limited to 'src/view/com/pager')
-rw-r--r-- | src/view/com/pager/FeedsTabBar.web.tsx | 8 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBarMobile.tsx | 18 | ||||
-rw-r--r-- | src/view/com/pager/Pager.tsx | 67 | ||||
-rw-r--r-- | src/view/com/pager/Pager.web.tsx | 11 | ||||
-rw-r--r-- | src/view/com/pager/PagerWithHeader.tsx | 212 | ||||
-rw-r--r-- | src/view/com/pager/TabBar.tsx | 11 |
6 files changed, 293 insertions, 34 deletions
diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index dc91bd296..25755bafe 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -1,10 +1,11 @@ -import React, {useMemo} from 'react' +import React from 'react' import {StyleSheet} from 'react-native' import Animated from 'react-native-reanimated' import {observer} from 'mobx-react-lite' import {TabBar} from 'view/com/pager/TabBar' import {RenderTabBarFnProps} from 'view/com/pager/Pager' import {useStores} from 'state/index' +import {useHomeTabs} from 'lib/hooks/useHomeTabs' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {FeedsTabBar as FeedsTabBarMobile} from './FeedsTabBarMobile' @@ -27,10 +28,7 @@ const FeedsTabBarTablet = observer(function FeedsTabBarTabletImpl( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { const store = useStores() - const items = useMemo( - () => ['Following', ...store.me.savedFeeds.pinnedFeedNames], - [store.me.savedFeeds.pinnedFeedNames], - ) + const items = useHomeTabs(store.preferences.pinnedFeeds) const pal = usePalette('default') const {headerMinimalShellTransform} = useMinimalShellMode() diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index d8579badc..d5de87081 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -1,9 +1,10 @@ -import React, {useMemo} from 'react' +import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' import {TabBar} from 'view/com/pager/TabBar' import {RenderTabBarFnProps} from 'view/com/pager/Pager' import {useStores} from 'state/index' +import {useHomeTabs} from 'lib/hooks/useHomeTabs' import {usePalette} from 'lib/hooks/usePalette' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {Link} from '../util/Link' @@ -18,9 +19,9 @@ import Animated from 'react-native-reanimated' export const FeedsTabBar = observer(function FeedsTabBarImpl( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { - const store = useStores() const pal = usePalette('default') - + const store = useStores() + const items = useHomeTabs(store.preferences.pinnedFeeds) const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3) const {headerMinimalShellTransform} = useMinimalShellMode() @@ -28,15 +29,6 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl( store.shell.openDrawer() }, [store]) - const items = useMemo( - () => ['Following', ...store.me.savedFeeds.pinnedFeedNames], - [store.me.savedFeeds.pinnedFeedNames], - ) - - const tabBarKey = useMemo(() => { - return items.join(',') - }, [items]) - return ( <Animated.View style={[ @@ -81,7 +73,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl( </View> </View> <TabBar - key={tabBarKey} + key={items.join(',')} onPressSelected={props.onPressSelected} selectedPage={props.selectedPage} onSelect={props.onSelect} diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx index 39ba29bda..531a41ee2 100644 --- a/src/view/com/pager/Pager.tsx +++ b/src/view/com/pager/Pager.tsx @@ -1,6 +1,10 @@ import React, {forwardRef} from 'react' import {Animated, View} from 'react-native' -import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view' +import PagerView, { + PagerViewOnPageSelectedEvent, + PagerViewOnPageScrollEvent, + PageScrollStateChangedNativeEvent, +} from 'react-native-pager-view' import {s} from 'lib/styles' export type PageSelectedEvent = PagerViewOnPageSelectedEvent @@ -21,6 +25,7 @@ interface Props { initialPage?: number renderTabBar: RenderTabBarFn onPageSelected?: (index: number) => void + onPageSelecting?: (index: number) => void testID?: string } export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>( @@ -31,11 +36,15 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>( initialPage = 0, renderTabBar, onPageSelected, + onPageSelecting, 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 pagerView = React.useRef<PagerView>(null) React.useImperativeHandle(ref, () => ({ @@ -50,15 +59,61 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>( [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) + lastDirection.current = 0 + } else if ( + lastDirection.current === 1 && + offset > lastOffset.current + ) { + onPageSelecting?.(position + 1) + lastDirection.current = 0 + } + } else { + if (offset < lastOffset.current) { + lastDirection.current = -1 + } else if (offset > lastOffset.current) { + lastDirection.current = 1 + } + } + lastOffset.current = offset + }, + [lastOffset, lastDirection, onPageSelecting], + ) + + const onPageScrollStateChanged = React.useCallback( + (e: PageScrollStateChangedNativeEvent) => { + scrollState.current = e.nativeEvent.pageScrollState + }, + [scrollState], + ) + const onTabBarSelect = React.useCallback( (index: number) => { pagerView.current?.setPage(index) + onPageSelecting?.(index) }, - [pagerView], + [pagerView, onPageSelecting], ) return ( - <View testID={testID}> + <View testID={testID} style={s.flex1}> {tabBarPosition === 'top' && renderTabBar({ selectedPage, @@ -66,9 +121,11 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>( })} <AnimatedPagerView ref={pagerView} - style={s.h100pct} + style={s.flex1} initialPage={initialPage} - onPageSelected={onPageSelectedInner}> + onPageScrollStateChanged={onPageScrollStateChanged} + onPageSelected={onPageSelectedInner} + onPageScroll={onPageScroll}> {children} </AnimatedPagerView> {tabBarPosition === 'bottom' && diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx index fe4febbb7..7ec292667 100644 --- a/src/view/com/pager/Pager.web.tsx +++ b/src/view/com/pager/Pager.web.tsx @@ -13,6 +13,7 @@ interface Props { initialPage?: number renderTabBar: RenderTabBarFn onPageSelected?: (index: number) => void + onPageSelecting?: (index: number) => void } export const Pager = React.forwardRef(function PagerImpl( { @@ -21,6 +22,7 @@ export const Pager = React.forwardRef(function PagerImpl( initialPage = 0, renderTabBar, onPageSelected, + onPageSelecting, }: React.PropsWithChildren<Props>, ref, ) { @@ -34,21 +36,20 @@ export const Pager = React.forwardRef(function PagerImpl( (index: number) => { setSelectedPage(index) onPageSelected?.(index) + onPageSelecting?.(index) }, - [setSelectedPage, onPageSelected], + [setSelectedPage, onPageSelected, onPageSelecting], ) return ( - <View> + <View style={s.hContentRegion}> {tabBarPosition === 'top' && renderTabBar({ selectedPage, onSelect: onTabBarSelect, })} {React.Children.map(children, (child, i) => ( - <View - style={selectedPage === i ? undefined : s.hidden} - key={`page-${i}`}> + <View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}> {child} </View> ))} diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx new file mode 100644 index 000000000..3cdd3ab2e --- /dev/null +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -0,0 +1,212 @@ +import * as React from 'react' +import {LayoutChangeEvent, StyleSheet} from 'react-native' +import Animated, { + Easing, + useAnimatedReaction, + useAnimatedScrollHandler, + useAnimatedStyle, + useSharedValue, + withTiming, + runOnJS, +} from 'react-native-reanimated' +import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' +import {TabBar} from './TabBar' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {OnScrollCb} from 'lib/hooks/useOnMainScroll' + +const SCROLLED_DOWN_LIMIT = 200 + +interface PagerWithHeaderChildParams { + headerHeight: number + onScroll: OnScrollCb + isScrolledDown: boolean +} + +export interface PagerWithHeaderProps { + testID?: string + children: + | (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[] + | ((props: PagerWithHeaderChildParams) => JSX.Element) + items: string[] + renderHeader?: () => JSX.Element + initialPage?: number + onPageSelected?: (index: number) => void + onCurrentPageSelected?: (index: number) => void +} +export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( + function PageWithHeaderImpl( + { + children, + testID, + items, + renderHeader, + initialPage, + onPageSelected, + onCurrentPageSelected, + }: PagerWithHeaderProps, + ref, + ) { + const {isMobile} = useWebMediaQueries() + const [currentPage, setCurrentPage] = React.useState(0) + const scrollYs = React.useRef<Record<number, number>>({}) + const scrollY = useSharedValue(scrollYs.current[currentPage] || 0) + const [tabBarHeight, setTabBarHeight] = React.useState(0) + const [headerHeight, setHeaderHeight] = React.useState(0) + const [isScrolledDown, setIsScrolledDown] = React.useState( + scrollYs.current[currentPage] > SCROLLED_DOWN_LIMIT, + ) + + // react to scroll updates + function onScrollUpdate(v: number) { + // track each page's current scroll position + scrollYs.current[currentPage] = Math.min(v, headerHeight - tabBarHeight) + // update the 'is scrolled down' value + setIsScrolledDown(v > SCROLLED_DOWN_LIMIT) + } + useAnimatedReaction( + () => scrollY.value, + v => runOnJS(onScrollUpdate)(v), + ) + + // capture the header bar sizing + const onTabBarLayout = React.useCallback( + (evt: LayoutChangeEvent) => { + setTabBarHeight(evt.nativeEvent.layout.height) + }, + [setTabBarHeight], + ) + const onHeaderLayout = React.useCallback( + (evt: LayoutChangeEvent) => { + setHeaderHeight(evt.nativeEvent.layout.height) + }, + [setHeaderHeight], + ) + + // render the the header and tab bar + const headerTransform = useAnimatedStyle( + () => ({ + transform: [ + { + translateY: Math.min( + Math.min(scrollY.value, headerHeight - tabBarHeight) * -1, + 0, + ), + }, + ], + }), + [scrollY, headerHeight, tabBarHeight], + ) + const renderTabBar = React.useCallback( + (props: RenderTabBarFnProps) => { + return ( + <Animated.View + onLayout={onHeaderLayout} + style={[ + isMobile ? styles.tabBarMobile : styles.tabBarDesktop, + headerTransform, + ]}> + {renderHeader?.()} + <TabBar + items={items} + selectedPage={currentPage} + onSelect={props.onSelect} + onPressSelected={onCurrentPageSelected} + onLayout={onTabBarLayout} + /> + </Animated.View> + ) + }, + [ + items, + renderHeader, + headerTransform, + currentPage, + onCurrentPageSelected, + isMobile, + onTabBarLayout, + onHeaderLayout, + ], + ) + + // props to pass into children render functions + const onScroll = useAnimatedScrollHandler({ + onScroll(e) { + scrollY.value = e.contentOffset.y + }, + }) + const childProps = React.useMemo<PagerWithHeaderChildParams>(() => { + return { + headerHeight, + onScroll, + isScrolledDown, + } + }, [headerHeight, onScroll, isScrolledDown]) + + const onPageSelectedInner = React.useCallback( + (index: number) => { + setCurrentPage(index) + onPageSelected?.(index) + }, + [onPageSelected, setCurrentPage], + ) + + const onPageSelecting = React.useCallback( + (index: number) => { + setCurrentPage(index) + if (scrollY.value > headerHeight) { + scrollY.value = headerHeight + } + scrollY.value = withTiming(scrollYs.current[index] || 0, { + duration: 170, + easing: Easing.inOut(Easing.quad), + }) + }, + [scrollY, setCurrentPage, scrollYs, headerHeight], + ) + + return ( + <Pager + ref={ref} + testID={testID} + initialPage={initialPage} + onPageSelected={onPageSelectedInner} + onPageSelecting={onPageSelecting} + renderTabBar={renderTabBar} + tabBarPosition="top"> + {toArray(children) + .filter(Boolean) + .map(child => { + if (child) { + return child(childProps) + } + return null + })} + </Pager> + ) + }, +) + +const styles = StyleSheet.create({ + tabBarMobile: { + position: 'absolute', + zIndex: 1, + top: 0, + left: 0, + width: '100%', + }, + tabBarDesktop: { + position: 'absolute', + zIndex: 1, + top: 0, + // @ts-ignore Web only -prf + left: 'calc(50% - 299px)', + width: 598, + }, +}) + +function toArray<T>(v: T | T[]): T[] { + if (Array.isArray(v)) { + return v + } + return [v] +} diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index 8614bdf64..662d73668 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -13,7 +13,8 @@ export interface TabBarProps { items: string[] indicatorColor?: string onSelect?: (index: number) => void - onPressSelected?: () => void + onPressSelected?: (index: number) => void + onLayout?: (evt: LayoutChangeEvent) => void } export function TabBar({ @@ -23,6 +24,7 @@ export function TabBar({ indicatorColor, onSelect, onPressSelected, + onLayout, }: TabBarProps) { const pal = usePalette('default') const scrollElRef = useRef<ScrollView>(null) @@ -44,7 +46,7 @@ export function TabBar({ (index: number) => { onSelect?.(index) if (index === selectedPage) { - onPressSelected?.() + onPressSelected?.(index) } }, [onSelect, selectedPage, onPressSelected], @@ -66,7 +68,7 @@ export function TabBar({ const styles = isDesktop || isTablet ? desktopStyles : mobileStyles return ( - <View testID={testID} style={[pal.view, styles.outer]}> + <View testID={testID} style={[pal.view, styles.outer]} onLayout={onLayout}> <DraggableScrollView horizontal={true} showsHorizontalScrollIndicator={false} @@ -118,10 +120,7 @@ const desktopStyles = StyleSheet.create({ const mobileStyles = StyleSheet.create({ outer: { - flex: 1, flexDirection: 'row', - backgroundColor: 'transparent', - maxWidth: '100%', }, contentContainer: { columnGap: isWeb ? 0 : 20, |