Factor out the bottom bar and layer it under the side menu (close #243) (#268)

zio/stable
Paul Frazee 2023-03-06 11:59:06 -06:00 committed by GitHub
parent c2a931f49c
commit f84a379376
3 changed files with 297 additions and 289 deletions

View File

@ -0,0 +1,292 @@
import React from 'react'
import {
Animated,
GestureResponderEvent,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {observer} from 'mobx-react-lite'
import {Text} from 'view/com/util/text/Text'
import {useStores} from 'state/index'
import {useAnalytics} from 'lib/analytics'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {TabPurpose, TabPurposeMainPath} from 'state/models/navigation'
import {clamp} from 'lib/numbers'
import {
HomeIcon,
HomeIconSolid,
MagnifyingGlassIcon2,
MagnifyingGlassIcon2Solid,
ComposeIcon2,
BellIcon,
BellIconSolid,
UserIcon,
} from 'lib/icons'
import {colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
export const BottomBar = observer(() => {
const store = useStores()
const pal = usePalette('default')
const minimalShellInterp = useAnimatedValue(0)
const safeAreaInsets = useSafeAreaInsets()
const {track} = useAnalytics()
React.useEffect(() => {
if (store.shell.minimalShellMode) {
Animated.timing(minimalShellInterp, {
toValue: 1,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
} else {
Animated.timing(minimalShellInterp, {
toValue: 0,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
}
}, [minimalShellInterp, store.shell.minimalShellMode])
const footerMinimalShellTransform = {
transform: [{translateY: Animated.multiply(minimalShellInterp, 100)}],
}
const onPressHome = React.useCallback(() => {
track('MobileShell:HomeButtonPressed')
if (store.nav.tab.fixedTabPurpose === TabPurpose.Default) {
if (!store.nav.tab.canGoBack) {
store.emitScreenSoftReset()
} else {
store.nav.tab.fixedTabReset()
}
} else {
store.nav.switchTo(TabPurpose.Default, false)
if (store.nav.tab.index === 0) {
store.nav.tab.fixedTabReset()
}
}
}, [store, track])
const onPressSearch = React.useCallback(() => {
track('MobileShell:SearchButtonPressed')
if (store.nav.tab.fixedTabPurpose === TabPurpose.Search) {
if (!store.nav.tab.canGoBack) {
store.emitScreenSoftReset()
} else {
store.nav.tab.fixedTabReset()
}
} else {
store.nav.switchTo(TabPurpose.Search, false)
if (store.nav.tab.index === 0) {
store.nav.tab.fixedTabReset()
}
}
}, [store, track])
const onPressCompose = React.useCallback(() => {
track('MobileShell:ComposeButtonPressed')
store.shell.openComposer({})
}, [store, track])
const onPressNotifications = React.useCallback(() => {
track('MobileShell:NotificationsButtonPressed')
if (store.nav.tab.fixedTabPurpose === TabPurpose.Notifs) {
if (!store.nav.tab.canGoBack) {
store.emitScreenSoftReset()
} else {
store.nav.tab.fixedTabReset()
}
} else {
store.nav.switchTo(TabPurpose.Notifs, false)
if (store.nav.tab.index === 0) {
store.nav.tab.fixedTabReset()
}
}
}, [store, track])
const onPressProfile = React.useCallback(() => {
track('MobileShell:ProfileButtonPressed')
store.nav.navigate(`/profile/${store.me.handle}`)
}, [store, track])
const isAtHome =
store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Default]
const isAtSearch =
store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Search]
const isAtNotifications =
store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Notifs]
return (
<Animated.View
style={[
styles.bottomBar,
pal.view,
pal.border,
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
footerMinimalShellTransform,
]}>
<Btn
icon={
isAtHome ? (
<HomeIconSolid
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
) : (
<HomeIcon
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
)
}
onPress={onPressHome}
/>
<Btn
icon={
isAtSearch ? (
<MagnifyingGlassIcon2Solid
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
) : (
<MagnifyingGlassIcon2
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
)
}
onPress={onPressSearch}
/>
<Btn
icon={
<View style={styles.ctrlIconSizingWrapper}>
<ComposeIcon2
strokeWidth={1.5}
size={29}
style={[styles.ctrlIcon, pal.text, styles.composeIcon]}
backgroundColor={pal.colors.background}
/>
</View>
}
onPress={onPressCompose}
/>
<Btn
icon={
isAtNotifications ? (
<BellIconSolid
size={24}
strokeWidth={1.9}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/>
) : (
<BellIcon
size={24}
strokeWidth={1.9}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/>
)
}
onPress={onPressNotifications}
notificationCount={store.me.notifications.unreadCount}
/>
<Btn
icon={
<View style={styles.ctrlIconSizingWrapper}>
<UserIcon
size={28}
strokeWidth={1.5}
style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
/>
</View>
}
onPress={onPressProfile}
/>
</Animated.View>
)
})
function Btn({
icon,
notificationCount,
onPress,
onLongPress,
}: {
icon: JSX.Element
notificationCount?: number
onPress?: (event: GestureResponderEvent) => void
onLongPress?: (event: GestureResponderEvent) => void
}) {
return (
<TouchableOpacity
style={styles.ctrl}
onPress={onLongPress ? onPress : undefined}
onPressIn={onLongPress ? undefined : onPress}
onLongPress={onLongPress}>
{notificationCount ? (
<View style={styles.notificationCount}>
<Text style={styles.notificationCountLabel}>{notificationCount}</Text>
</View>
) : undefined}
{icon}
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
bottomBar: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
flexDirection: 'row',
borderTopWidth: 1,
paddingLeft: 5,
paddingRight: 10,
},
ctrl: {
flex: 1,
paddingTop: 13,
paddingBottom: 4,
},
notificationCount: {
position: 'absolute',
left: '56%',
top: 10,
backgroundColor: colors.blue3,
paddingHorizontal: 4,
paddingBottom: 1,
borderRadius: 8,
zIndex: 1,
},
notificationCountLabel: {
fontSize: 12,
fontWeight: 'bold',
color: colors.white,
},
ctrlIcon: {
marginLeft: 'auto',
marginRight: 'auto',
},
ctrlIconSizingWrapper: {
height: 27,
},
homeIcon: {
top: 0,
},
searchIcon: {
top: -2,
},
bellIcon: {
top: -2.5,
},
composeIcon: {
top: -4.5,
},
profileIcon: {
top: -4,
},
})

View File

@ -119,7 +119,6 @@ export const Menu = observer(({onClose}: {onClose: () => void}) => {
style={[
styles.view,
theme.colorScheme === 'light' ? pal.view : styles.viewDarkMode,
store.shell.minimalShellMode && styles.viewMinimalShell,
]}>
<TouchableOpacity
testID="profileCardButton"
@ -272,15 +271,12 @@ const styles = StyleSheet.create({
view: {
flex: 1,
paddingTop: 10,
paddingBottom: 90,
paddingBottom: 50,
paddingLeft: 30,
},
viewDarkMode: {
backgroundColor: colors.gray8,
},
viewMinimalShell: {
paddingBottom: 50,
},
profileCardDisplayName: {
marginTop: 20,

View File

@ -1,11 +1,9 @@
import React, {useState, useEffect} from 'react'
import React, {useState} from 'react'
import {observer} from 'mobx-react-lite'
import {
Animated,
GestureResponderEvent,
StatusBar,
StyleSheet,
TouchableOpacity,
TouchableWithoutFeedback,
useWindowDimensions,
View,
@ -14,14 +12,11 @@ import {ScreenContainer, Screen} from 'react-native-screens'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {useStores} from 'state/index'
import {
NavigationModel,
TabPurpose,
TabPurposeMainPath,
} from 'state/models/navigation'
import {NavigationModel} from 'state/models/navigation'
import {match, MatchResult} from '../../routes'
import {Login} from '../../screens/Login'
import {Menu} from './Menu'
import {BottomBar} from './BottomBar'
import {HorzSwipe} from '../../com/util/gestures/HorzSwipe'
import {ModalsContainer} from '../../com/modals/Modal'
import {Lightbox} from '../../com/lightbox/Lightbox'
@ -29,48 +24,9 @@ import {Text} from '../../com/util/text/Text'
import {ErrorBoundary} from '../../com/util/ErrorBoundary'
import {Composer} from './Composer'
import {s, colors} from 'lib/styles'
import {clamp} from 'lib/numbers'
import {
HomeIcon,
HomeIconSolid,
MagnifyingGlassIcon2,
MagnifyingGlassIcon2Solid,
ComposeIcon2,
BellIcon,
BellIconSolid,
UserIcon,
} from 'lib/icons'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics'
const Btn = ({
icon,
notificationCount,
onPress,
onLongPress,
}: {
icon: JSX.Element
notificationCount?: number
onPress?: (event: GestureResponderEvent) => void
onLongPress?: (event: GestureResponderEvent) => void
}) => {
return (
<TouchableOpacity
style={styles.ctrl}
onPress={onLongPress ? onPress : undefined}
onPressIn={onLongPress ? undefined : onPress}
onLongPress={onLongPress}>
{notificationCount ? (
<View style={styles.notificationCount}>
<Text style={styles.notificationCountLabel}>{notificationCount}</Text>
</View>
) : undefined}
{icon}
</TouchableOpacity>
)
}
export const MobileShell: React.FC = observer(() => {
const theme = useTheme()
@ -79,87 +35,8 @@ export const MobileShell: React.FC = observer(() => {
const winDim = useWindowDimensions()
const [menuSwipingDirection, setMenuSwipingDirection] = useState(0)
const swipeGestureInterp = useAnimatedValue(0)
const minimalShellInterp = useAnimatedValue(0)
const safeAreaInsets = useSafeAreaInsets()
const screenRenderDesc = constructScreenRenderDesc(store.nav)
const {track} = useAnalytics()
const onPressHome = () => {
track('MobileShell:HomeButtonPressed')
if (store.nav.tab.fixedTabPurpose === TabPurpose.Default) {
if (!store.nav.tab.canGoBack) {
store.emitScreenSoftReset()
} else {
store.nav.tab.fixedTabReset()
}
} else {
store.nav.switchTo(TabPurpose.Default, false)
if (store.nav.tab.index === 0) {
store.nav.tab.fixedTabReset()
}
}
}
const onPressSearch = () => {
track('MobileShell:SearchButtonPressed')
if (store.nav.tab.fixedTabPurpose === TabPurpose.Search) {
if (!store.nav.tab.canGoBack) {
store.emitScreenSoftReset()
} else {
store.nav.tab.fixedTabReset()
}
} else {
store.nav.switchTo(TabPurpose.Search, false)
if (store.nav.tab.index === 0) {
store.nav.tab.fixedTabReset()
}
}
}
const onPressCompose = () => {
track('MobileShell:ComposeButtonPressed')
store.shell.openComposer({})
}
const onPressNotifications = () => {
track('MobileShell:NotificationsButtonPressed')
if (store.nav.tab.fixedTabPurpose === TabPurpose.Notifs) {
if (!store.nav.tab.canGoBack) {
store.emitScreenSoftReset()
} else {
store.nav.tab.fixedTabReset()
}
} else {
store.nav.switchTo(TabPurpose.Notifs, false)
if (store.nav.tab.index === 0) {
store.nav.tab.fixedTabReset()
}
}
}
const onPressProfile = () => {
track('MobileShell:ProfileButtonPressed')
store.nav.navigate(`/profile/${store.me.handle}`)
}
// minimal shell animation
// =
useEffect(() => {
if (store.shell.minimalShellMode) {
Animated.timing(minimalShellInterp, {
toValue: 1,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
} else {
Animated.timing(minimalShellInterp, {
toValue: 0,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
}
}, [minimalShellInterp, store.shell.minimalShellMode])
const footerMinimalShellTransform = {
transform: [{translateY: Animated.multiply(minimalShellInterp, 100)}],
}
// navigation swipes
// =
@ -231,13 +108,6 @@ export const MobileShell: React.FC = observer(() => {
}),
}
: undefined
// TODO
// const tabMenuTransform = {
// transform: [{translateY: Animated.multiply(tabMenuInterp, -320)}],
// }
// const newTabTransform = {
// transform: [{scale: newTabInterp}],
// }
if (store.hackUpgradeNeeded) {
return (
@ -286,13 +156,6 @@ export const MobileShell: React.FC = observer(() => {
)
}
const isAtHome =
store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Default]
const isAtSearch =
store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Search]
const isAtNotifications =
store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Notifs]
const screenBg = {
backgroundColor: theme.colorScheme === 'dark' ? colors.gray7 : colors.gray1,
}
@ -360,6 +223,7 @@ export const MobileShell: React.FC = observer(() => {
},
)}
</ScreenContainer>
<BottomBar />
{isMenuActive || menuSwipingDirection !== 0 ? (
<TouchableWithoutFeedback
onPress={() => store.shell.setMainMenuOpen(false)}>
@ -373,95 +237,6 @@ export const MobileShell: React.FC = observer(() => {
)}
</HorzSwipe>
</View>
<Animated.View
style={[
styles.bottomBar,
pal.view,
pal.border,
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
footerMinimalShellTransform,
]}>
<Btn
icon={
isAtHome ? (
<HomeIconSolid
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
) : (
<HomeIcon
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
)
}
onPress={onPressHome}
/>
<Btn
icon={
isAtSearch ? (
<MagnifyingGlassIcon2Solid
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
) : (
<MagnifyingGlassIcon2
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
)
}
onPress={onPressSearch}
/>
<Btn
icon={
<View style={styles.ctrlIconSizingWrapper}>
<ComposeIcon2
strokeWidth={1.5}
size={29}
style={[styles.ctrlIcon, pal.text, styles.composeIcon]}
backgroundColor={pal.colors.background}
/>
</View>
}
onPress={onPressCompose}
/>
<Btn
icon={
isAtNotifications ? (
<BellIconSolid
size={24}
strokeWidth={1.9}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/>
) : (
<BellIcon
size={24}
strokeWidth={1.9}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/>
)
}
onPress={onPressNotifications}
notificationCount={store.me.notifications.unreadCount}
/>
<Btn
icon={
<View style={styles.ctrlIconSizingWrapper}>
<UserIcon
size={28}
strokeWidth={1.5}
style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
/>
</View>
}
onPress={onPressProfile}
/>
</Animated.View>
<ModalsContainer />
<Lightbox />
<Composer
@ -563,59 +338,4 @@ const styles = StyleSheet.create({
topBarProtectorDark: {
backgroundColor: colors.black,
},
bottomBar: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
flexDirection: 'row',
borderTopWidth: 1,
paddingLeft: 5,
paddingRight: 10,
},
ctrl: {
flex: 1,
paddingTop: 13,
paddingBottom: 4,
},
notificationCount: {
position: 'absolute',
left: '56%',
top: 10,
backgroundColor: colors.blue3,
paddingHorizontal: 4,
paddingBottom: 1,
borderRadius: 8,
zIndex: 1,
},
notificationCountLabel: {
fontSize: 12,
fontWeight: 'bold',
color: colors.white,
},
ctrlIcon: {
marginLeft: 'auto',
marginRight: 'auto',
},
ctrlIconSizingWrapper: {
height: 27,
},
inactive: {
color: colors.gray3,
},
homeIcon: {
top: 0,
},
searchIcon: {
top: -2,
},
bellIcon: {
top: -2.5,
},
composeIcon: {
top: -4.5,
},
profileIcon: {
top: -4,
},
})