diff options
Diffstat (limited to 'src/view/com/pager')
-rw-r--r-- | src/view/com/pager/FeedsTabBar.tsx | 64 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBar.web.tsx | 22 | ||||
-rw-r--r-- | src/view/com/pager/Pager.tsx | 87 | ||||
-rw-r--r-- | src/view/com/pager/Pager.web.tsx | 69 | ||||
-rw-r--r-- | src/view/com/pager/TabBar.tsx | 161 |
5 files changed, 403 insertions, 0 deletions
diff --git a/src/view/com/pager/FeedsTabBar.tsx b/src/view/com/pager/FeedsTabBar.tsx new file mode 100644 index 000000000..9831218ec --- /dev/null +++ b/src/view/com/pager/FeedsTabBar.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import {Animated, StyleSheet, TouchableOpacity} from 'react-native' +import {observer} from 'mobx-react-lite' +import {TabBar} from 'view/com/pager/TabBar' +import {RenderTabBarFnProps} from 'view/com/pager/Pager' +import {UserAvatar} from '../util/UserAvatar' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' + +export const FeedsTabBar = observer( + (props: RenderTabBarFnProps & {onPressSelected: () => void}) => { + const store = useStores() + const pal = usePalette('default') + const interp = useAnimatedValue(0) + + React.useEffect(() => { + Animated.timing(interp, { + toValue: store.shell.minimalShellMode ? 1 : 0, + duration: 100, + useNativeDriver: true, + isInteraction: false, + }).start() + }, [interp, store.shell.minimalShellMode]) + const transform = { + transform: [{translateY: Animated.multiply(interp, -100)}], + } + + const onPressAvi = React.useCallback(() => { + store.shell.openDrawer() + }, [store]) + + return ( + <Animated.View style={[pal.view, styles.tabBar, transform]}> + <TouchableOpacity style={styles.tabBarAvi} onPress={onPressAvi}> + <UserAvatar avatar={store.me.avatar} size={30} /> + </TouchableOpacity> + <TabBar + {...props} + items={['Following', "What's hot"]} + indicatorPosition="bottom" + indicatorColor={pal.colors.link} + /> + </Animated.View> + ) + }, +) + +const styles = StyleSheet.create({ + tabBar: { + position: 'absolute', + zIndex: 1, + left: 0, + right: 0, + top: 0, + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 18, + }, + tabBarAvi: { + marginTop: 1, + marginRight: 18, + }, +}) diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx new file mode 100644 index 000000000..fc5932883 --- /dev/null +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import {observer} from 'mobx-react-lite' +import {TabBar} from 'view/com/pager/TabBar' +import {CenteredView} from 'view/com/util/Views' +import {RenderTabBarFnProps} from 'view/com/pager/Pager' +import {usePalette} from 'lib/hooks/usePalette' + +export const FeedsTabBar = observer( + (props: RenderTabBarFnProps & {onPressSelected: () => void}) => { + const pal = usePalette('default') + return ( + <CenteredView> + <TabBar + {...props} + items={['Following', "What's hot"]} + indicatorPosition="bottom" + indicatorColor={pal.colors.link} + /> + </CenteredView> + ) + }, +) diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx new file mode 100644 index 000000000..416828a27 --- /dev/null +++ b/src/view/com/pager/Pager.tsx @@ -0,0 +1,87 @@ +import React from 'react' +import {Animated, View} from 'react-native' +import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {s} from 'lib/styles' + +export type PageSelectedEvent = PagerViewOnPageSelectedEvent +const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) + +export interface RenderTabBarFnProps { + selectedPage: number + position: Animated.Value + offset: Animated.Value + onSelect?: (index: number) => void +} +export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element + +interface Props { + tabBarPosition?: 'top' | 'bottom' + initialPage?: number + renderTabBar: RenderTabBarFn + onPageSelected?: (index: number) => void +} +export const Pager = ({ + children, + tabBarPosition = 'top', + initialPage = 0, + renderTabBar, + onPageSelected, +}: React.PropsWithChildren<Props>) => { + const [selectedPage, setSelectedPage] = React.useState(0) + const position = useAnimatedValue(0) + const offset = useAnimatedValue(0) + const pagerView = React.useRef<PagerView>() + + const onPageSelectedInner = React.useCallback( + (e: PageSelectedEvent) => { + setSelectedPage(e.nativeEvent.position) + onPageSelected?.(e.nativeEvent.position) + }, + [setSelectedPage, onPageSelected], + ) + + const onTabBarSelect = React.useCallback( + (index: number) => { + pagerView.current?.setPage(index) + }, + [pagerView], + ) + + return ( + <View> + {tabBarPosition === 'top' && + renderTabBar({ + selectedPage, + position, + offset, + onSelect: onTabBarSelect, + })} + <AnimatedPagerView + ref={pagerView} + style={s.h100pct} + initialPage={initialPage} + onPageSelected={onPageSelectedInner} + onPageScroll={Animated.event( + [ + { + nativeEvent: { + position: position, + offset: offset, + }, + }, + ], + {useNativeDriver: true}, + )}> + {children} + </AnimatedPagerView> + {tabBarPosition === 'bottom' && + renderTabBar({ + selectedPage, + position, + offset, + onSelect: onTabBarSelect, + })} + </View> + ) +} diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx new file mode 100644 index 000000000..3c2805833 --- /dev/null +++ b/src/view/com/pager/Pager.web.tsx @@ -0,0 +1,69 @@ +import React from 'react' +import {Animated, View} from 'react-native' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {s} from 'lib/styles' + +export interface RenderTabBarFnProps { + selectedPage: number + position: Animated.Value + offset: Animated.Value + onSelect?: (index: number) => void +} +export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element + +interface Props { + tabBarPosition?: 'top' | 'bottom' + initialPage?: number + renderTabBar: RenderTabBarFn + onPageSelected?: (index: number) => void +} +export const Pager = ({ + children, + tabBarPosition = 'top', + initialPage = 0, + renderTabBar, + onPageSelected, +}: React.PropsWithChildren<Props>) => { + const [selectedPage, setSelectedPage] = React.useState(initialPage) + const position = useAnimatedValue(0) + const offset = useAnimatedValue(0) + + const onTabBarSelect = React.useCallback( + (index: number) => { + setSelectedPage(index) + onPageSelected?.(index) + Animated.timing(position, { + toValue: index, + duration: 200, + useNativeDriver: true, + }).start() + }, + [setSelectedPage, onPageSelected, position], + ) + + return ( + <View> + {tabBarPosition === 'top' && + renderTabBar({ + selectedPage, + position, + offset, + onSelect: onTabBarSelect, + })} + {children.map((child, i) => ( + <View + style={selectedPage === i ? undefined : s.hidden} + key={`page-${i}`}> + {child} + </View> + ))} + {tabBarPosition === 'bottom' && + renderTabBar({ + selectedPage, + position, + offset, + onSelect: onTabBarSelect, + })} + </View> + ) +} diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx new file mode 100644 index 000000000..0b45d95f5 --- /dev/null +++ b/src/view/com/pager/TabBar.tsx @@ -0,0 +1,161 @@ +import React, {createRef, useState, useMemo} from 'react' +import { + Animated, + StyleSheet, + TouchableWithoutFeedback, + View, +} from 'react-native' +import {Text} from '../util/text/Text' +import {usePalette} from 'lib/hooks/usePalette' +import {isDesktopWeb} from 'platform/detection' + +interface Layout { + x: number + width: number +} + +export interface TabBarProps { + selectedPage: number + items: string[] + position: Animated.Value + offset: Animated.Value + indicatorPosition?: 'top' | 'bottom' + indicatorColor?: string + onSelect?: (index: number) => void + onPressSelected?: () => void +} + +export function TabBar({ + selectedPage, + items, + position, + offset, + indicatorPosition = 'bottom', + indicatorColor, + onSelect, + onPressSelected, +}: TabBarProps) { + const pal = usePalette('default') + const [itemLayouts, setItemLayouts] = useState<Layout[]>( + items.map(() => ({x: 0, width: 0})), + ) + const itemRefs = useMemo( + () => Array.from({length: items.length}).map(() => createRef<View>()), + [items.length], + ) + const panX = Animated.add(position, offset) + + const indicatorStyle = { + backgroundColor: indicatorColor || pal.colors.link, + bottom: + indicatorPosition === 'bottom' ? (isDesktopWeb ? 0 : -1) : undefined, + top: indicatorPosition === 'top' ? (isDesktopWeb ? 0 : -1) : undefined, + transform: [ + { + translateX: panX.interpolate({ + inputRange: items.map((_item, i) => i), + outputRange: itemLayouts.map(l => l.x + l.width / 2), + }), + }, + { + scaleX: panX.interpolate({ + inputRange: items.map((_item, i) => i), + outputRange: itemLayouts.map(l => l.width), + }), + }, + ], + } + + const onLayout = () => { + const promises = [] + for (let i = 0; i < items.length; i++) { + promises.push( + new Promise<Layout>(resolve => { + itemRefs[i].current?.measure( + (x: number, _y: number, width: number) => { + resolve({x, width}) + }, + ) + }), + ) + } + Promise.all(promises).then((layouts: Layout[]) => { + setItemLayouts(layouts) + }) + } + + const onPressItem = (index: number) => { + onSelect?.(index) + if (index === selectedPage) { + onPressSelected?.() + } + } + + return ( + <View style={[pal.view, styles.outer]} onLayout={onLayout}> + <Animated.View style={[styles.indicator, indicatorStyle]} /> + {items.map((item, i) => { + const selected = i === selectedPage + return ( + <TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}> + <View + style={ + indicatorPosition === 'top' ? styles.itemTop : styles.itemBottom + } + ref={itemRefs[i]}> + <Text type="xl-bold" style={selected ? pal.text : pal.textLight}> + {item} + </Text> + </View> + </TouchableWithoutFeedback> + ) + })} + </View> + ) +} + +const styles = isDesktopWeb + ? StyleSheet.create({ + outer: { + flexDirection: 'row', + paddingHorizontal: 18, + }, + itemTop: { + paddingTop: 16, + paddingBottom: 14, + marginRight: 24, + }, + itemBottom: { + paddingTop: 14, + paddingBottom: 16, + marginRight: 24, + }, + indicator: { + position: 'absolute', + left: 0, + width: 1, + height: 3, + }, + }) + : StyleSheet.create({ + outer: { + flexDirection: 'row', + paddingHorizontal: 14, + }, + itemTop: { + paddingTop: 10, + paddingBottom: 10, + marginRight: 24, + }, + itemBottom: { + paddingTop: 8, + paddingBottom: 12, + marginRight: 24, + }, + indicator: { + position: 'absolute', + left: 0, + width: 1, + height: 3, + }, + }) |