Move the feed selector to the footer

zio/stable
Paul Frazee 2023-03-17 14:03:16 -05:00
parent 244b06c19d
commit c3ed0dc44c
6 changed files with 121 additions and 40 deletions

View File

@ -1,16 +1,18 @@
import React from 'react' import React from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {Button} from '../util/forms/Button' import {Button, ButtonType} from '../util/forms/Button'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import * as apilib from 'lib/api/index' import * as apilib from 'lib/api/index'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
const FollowButton = observer( const FollowButton = observer(
({ ({
type = 'inverted',
did, did,
declarationCid, declarationCid,
onToggleFollow, onToggleFollow,
}: { }: {
type?: ButtonType
did: string did: string
declarationCid: string declarationCid: string
onToggleFollow?: (v: boolean) => void onToggleFollow?: (v: boolean) => void
@ -42,7 +44,7 @@ const FollowButton = observer(
return ( return (
<Button <Button
type={isFollowing ? 'default' : 'inverted'} type={isFollowing ? 'default' : type}
onPress={onToggleFollowInner} onPress={onToggleFollowInner}
label={isFollowing ? 'Unfollow' : 'Follow'} label={isFollowing ? 'Unfollow' : 'Follow'}
/> />

View File

@ -15,11 +15,13 @@ export interface TabBarProps {
} }
interface Props { interface Props {
tabBarPosition?: 'top' | 'bottom'
renderTabBar: (props: TabBarProps) => JSX.Element renderTabBar: (props: TabBarProps) => JSX.Element
onPageSelected?: (e: PageSelectedEvent) => void onPageSelected?: (e: PageSelectedEvent) => void
} }
export const Pager = ({ export const Pager = ({
children, children,
tabBarPosition = 'top',
renderTabBar, renderTabBar,
onPageSelected, onPageSelected,
}: React.PropsWithChildren<Props>) => { }: React.PropsWithChildren<Props>) => {
@ -45,7 +47,13 @@ export const Pager = ({
return ( return (
<View> <View>
{renderTabBar({selectedPage, position, offset, onSelect: onTabBarSelect})} {tabBarPosition === 'top' &&
renderTabBar({
selectedPage,
position,
offset,
onSelect: onTabBarSelect,
})}
<AnimatedPagerView <AnimatedPagerView
ref={pagerView} ref={pagerView}
style={s.h100pct} style={s.h100pct}
@ -64,6 +72,13 @@ export const Pager = ({
)}> )}>
{children} {children}
</AnimatedPagerView> </AnimatedPagerView>
{tabBarPosition === 'bottom' &&
renderTabBar({
selectedPage,
position,
offset,
onSelect: onTabBarSelect,
})}
</View> </View>
) )
} }

View File

@ -77,6 +77,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
<View> <View>
<FollowButton <FollowButton
type="default"
did={opts.did} did={opts.did}
declarationCid={opts.declarationCid} declarationCid={opts.declarationCid}
onToggleFollow={onToggleFollow} onToggleFollow={onToggleFollow}

View File

@ -18,12 +18,16 @@ export function TabBar({
items, items,
position, position,
offset, offset,
indicatorPosition = 'bottom',
indicatorColor,
onSelect, onSelect,
}: { }: {
selectedPage: number selectedPage: number
items: string[] items: string[]
position: Animated.Value position: Animated.Value
offset: Animated.Value offset: Animated.Value
indicatorPosition?: 'top' | 'bottom'
indicatorColor?: string
onSelect?: (index: number) => void onSelect?: (index: number) => void
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
@ -36,8 +40,10 @@ export function TabBar({
) )
const panX = Animated.add(position, offset) const panX = Animated.add(position, offset)
const underlineStyle = { const indicatorStyle = {
backgroundColor: pal.colors.link, backgroundColor: indicatorColor || pal.colors.link,
bottom: indicatorPosition === 'bottom' ? -1 : undefined,
top: indicatorPosition === 'top' ? -1 : undefined,
left: panX.interpolate({ left: panX.interpolate({
inputRange: items.map((_item, i) => i), inputRange: items.map((_item, i) => i),
outputRange: itemLayouts.map(l => l.x), outputRange: itemLayouts.map(l => l.x),
@ -72,12 +78,16 @@ export function TabBar({
return ( return (
<View style={[pal.view, styles.outer]} onLayout={onLayout}> <View style={[pal.view, styles.outer]} onLayout={onLayout}>
<Animated.View style={[styles.underline, underlineStyle]} /> <Animated.View style={[styles.indicator, indicatorStyle]} />
{items.map((item, i) => { {items.map((item, i) => {
const selected = i === selectedPage const selected = i === selectedPage
return ( return (
<TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}> <TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}>
<View style={styles.item} ref={itemRefs[i]}> <View
style={
indicatorPosition === 'top' ? styles.itemTop : styles.itemBottom
}
ref={itemRefs[i]}>
<Text type="xl-bold" style={selected ? pal.text : pal.textLight}> <Text type="xl-bold" style={selected ? pal.text : pal.textLight}>
{item} {item}
</Text> </Text>
@ -94,15 +104,19 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
paddingHorizontal: 14, paddingHorizontal: 14,
}, },
item: { itemTop: {
paddingTop: 10,
paddingBottom: 10,
marginRight: 24,
},
itemBottom: {
paddingTop: 8, paddingTop: 8,
paddingBottom: 12, paddingBottom: 12,
marginRight: 24, marginRight: 24,
}, },
underline: { indicator: {
position: 'absolute', position: 'absolute',
height: 3, height: 3,
bottom: -1,
borderRadius: 4, borderRadius: 4,
}, },
}) })

View File

@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import { import {
Animated,
FlatList, FlatList,
StyleSheet, StyleSheet,
TouchableOpacity,
View, View,
useWindowDimensions, useWindowDimensions,
} from 'react-native' } from 'react-native'
@ -15,7 +15,6 @@ import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {Feed} from '../com/posts/Feed' import {Feed} from '../com/posts/Feed'
import {LoadLatestBtn} from '../com/util/LoadLatestBtn' import {LoadLatestBtn} from '../com/util/LoadLatestBtn'
import {WelcomeBanner} from '../com/util/WelcomeBanner' import {WelcomeBanner} from '../com/util/WelcomeBanner'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {TabBar} from 'view/com/util/TabBar' import {TabBar} from 'view/com/util/TabBar'
import {Pager, PageSelectedEvent, TabBarProps} from 'view/com/util/Pager' import {Pager, PageSelectedEvent, TabBarProps} from 'view/com/util/Pager'
import {FAB} from '../com/util/FAB' import {FAB} from '../com/util/FAB'
@ -23,15 +22,18 @@ import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {useAnalytics} from 'lib/analytics' import {useAnalytics} from 'lib/analytics'
import {ComposeIcon2} from 'lib/icons' import {ComposeIcon2} from 'lib/icons'
import {clamp} from 'lodash'
const TAB_BAR_HEIGHT = 82 const TAB_BAR_HEIGHT = 82
const BOTTOM_BAR_HEIGHT = 48
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export const HomeScreen = withAuthRequired((_opts: Props) => { export const HomeScreen = withAuthRequired((_opts: Props) => {
const store = useStores() const store = useStores()
const pal = usePalette('default')
const [selectedPage, setSelectedPage] = React.useState(0) const [selectedPage, setSelectedPage] = React.useState(0)
useFocusEffect( useFocusEffect(
@ -51,26 +53,15 @@ export const HomeScreen = withAuthRequired((_opts: Props) => {
[store], [store],
) )
const onPressAvi = React.useCallback(() => { const renderTabBar = React.useCallback((props: TabBarProps) => {
store.shell.openDrawer() return <FloatingTabBar {...props} />
}, [store]) }, [])
const renderTabBar = React.useCallback(
(props: TabBarProps) => {
return (
<View style={[pal.view, pal.border, styles.tabBar]}>
<TouchableOpacity style={styles.tabBarAvi} onPress={onPressAvi}>
<UserAvatar avatar={store.me.avatar} size={32} />
</TouchableOpacity>
<TabBar items={['Suggested', 'Following']} {...props} />
</View>
)
},
[store.me.avatar, pal, onPressAvi],
)
return ( return (
<Pager onPageSelected={onPageSelected} renderTabBar={renderTabBar}> <Pager
onPageSelected={onPageSelected}
renderTabBar={renderTabBar}
tabBarPosition="bottom">
<AlgoView key="1" /> <AlgoView key="1" />
<View key="2"> <View key="2">
<FollowingView /> <FollowingView />
@ -79,6 +70,46 @@ export const HomeScreen = withAuthRequired((_opts: Props) => {
) )
}) })
const FloatingTabBar = observer((props: TabBarProps) => {
const store = useStores()
const safeAreaInsets = useSafeAreaInsets()
const pal = usePalette('default')
const interp = useAnimatedValue(0)
const pad = React.useMemo(
() => ({
paddingBottom: clamp(safeAreaInsets.bottom, 15, 20),
}),
[safeAreaInsets],
)
React.useEffect(() => {
Animated.timing(interp, {
toValue: store.shell.minimalShellMode ? 0 : 1,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
}, [interp, store.shell.minimalShellMode])
const transform = {
transform: [
{translateY: Animated.multiply(interp, -1 * BOTTOM_BAR_HEIGHT)},
],
}
return (
<Animated.View
style={[pal.view, pal.border, styles.tabBar, pad, transform]}>
<TabBar
items={['Suggested', 'Following']}
{...props}
indicatorPosition="top"
indicatorColor={pal.colors.link}
/>
</Animated.View>
)
})
const AlgoView = observer(() => { const AlgoView = observer(() => {
const store = useStores() const store = useStores()
const onMainScroll = useOnMainScroll(store) const onMainScroll = useOnMainScroll(store)
@ -270,13 +301,19 @@ const FollowingView = observer(() => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
tabBar: { tabBar: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 18, paddingHorizontal: 8,
borderBottomWidth: 1, borderTopWidth: 1,
paddingTop: 0,
paddingBottom: 30,
// height: 100,
}, },
tabBarAvi: { tabBarAvi: {
marginRight: 16, marginRight: 4,
paddingBottom: 2,
}, },
}) })

View File

@ -34,16 +34,24 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
const minimalShellInterp = useAnimatedValue(0) const minimalShellInterp = useAnimatedValue(0)
const safeAreaInsets = useSafeAreaInsets() const safeAreaInsets = useSafeAreaInsets()
const {track} = useAnalytics() const {track} = useAnalytics()
const {isAtHome, isAtSearch, isAtNotifications} = useNavigationState( const {isAtHome, isAtSearch, isAtNotifications, noBorder} =
state => { useNavigationState(state => {
return { const res = {
isAtHome: getTabState(state, 'Home') !== TabState.Outside, isAtHome: getTabState(state, 'Home') !== TabState.Outside,
isAtSearch: getTabState(state, 'Search') !== TabState.Outside, isAtSearch: getTabState(state, 'Search') !== TabState.Outside,
isAtNotifications: isAtNotifications:
getTabState(state, 'Notifications') !== TabState.Outside, getTabState(state, 'Notifications') !== TabState.Outside,
noBorder: getTabState(state, 'Home') === TabState.InsideAtRoot,
} }
}, if (!res.isAtHome && !res.isAtNotifications && !res.isAtSearch) {
) // HACK for some reason useNavigationState will give us pre-hydration results
// and not update after, so we force isAtHome if all came back false
// -prf
res.isAtHome = true
res.noBorder = true
}
return res
})
React.useEffect(() => { React.useEffect(() => {
if (store.shell.minimalShellMode) { if (store.shell.minimalShellMode) {
@ -99,6 +107,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
<Animated.View <Animated.View
style={[ style={[
styles.bottomBar, styles.bottomBar,
noBorder && styles.noBorder,
pal.view, pal.view,
pal.border, pal.border,
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)}, {paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
@ -213,6 +222,9 @@ const styles = StyleSheet.create({
paddingLeft: 5, paddingLeft: 5,
paddingRight: 10, paddingRight: 10,
}, },
noBorder: {
borderTopWidth: 0,
},
ctrl: { ctrl: {
flex: 1, flex: 1,
paddingTop: 13, paddingTop: 13,