about summary refs log tree commit diff
path: root/src/view/shell/bottom-bar
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/shell/bottom-bar')
-rw-r--r--src/view/shell/bottom-bar/BottomBar.tsx198
-rw-r--r--src/view/shell/bottom-bar/BottomBarStyles.tsx61
-rw-r--r--src/view/shell/bottom-bar/BottomBarWeb.tsx101
3 files changed, 360 insertions, 0 deletions
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
new file mode 100644
index 000000000..59b21968d
--- /dev/null
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -0,0 +1,198 @@
+import React from 'react'
+import {
+  Animated,
+  GestureResponderEvent,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import {StackActions, useNavigationState} from '@react-navigation/native'
+import {BottomTabBarProps} from '@react-navigation/bottom-tabs'
+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 {clamp} from 'lib/numbers'
+import {
+  HomeIcon,
+  HomeIconSolid,
+  MagnifyingGlassIcon2,
+  MagnifyingGlassIcon2Solid,
+  BellIcon,
+  BellIconSolid,
+  UserIcon,
+} from 'lib/icons'
+import {usePalette} from 'lib/hooks/usePalette'
+import {getTabState, TabState} from 'lib/routes/helpers'
+import {styles} from './BottomBarStyles'
+import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
+
+export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
+  const store = useStores()
+  const pal = usePalette('default')
+  const safeAreaInsets = useSafeAreaInsets()
+  const {track} = useAnalytics()
+  const {isAtHome, isAtSearch, isAtNotifications} = useNavigationState(
+    state => {
+      const res = {
+        isAtHome: getTabState(state, 'Home') !== TabState.Outside,
+        isAtSearch: getTabState(state, 'Search') !== TabState.Outside,
+        isAtNotifications:
+          getTabState(state, 'Notifications') !== TabState.Outside,
+      }
+      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
+      }
+      return res
+    },
+  )
+
+  const {footerMinimalShellTransform} = useMinimalShellMode()
+
+  const onPressTab = React.useCallback(
+    (tab: string) => {
+      track(`MobileShell:${tab}ButtonPressed`)
+      const state = navigation.getState()
+      const tabState = getTabState(state, tab)
+      if (tabState === TabState.InsideAtRoot) {
+        store.emitScreenSoftReset()
+      } else if (tabState === TabState.Inside) {
+        navigation.dispatch(StackActions.popToTop())
+      } else {
+        navigation.navigate(`${tab}Tab`)
+      }
+    },
+    [store, track, navigation],
+  )
+  const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
+  const onPressSearch = React.useCallback(
+    () => onPressTab('Search'),
+    [onPressTab],
+  )
+  const onPressNotifications = React.useCallback(
+    () => onPressTab('Notifications'),
+    [onPressTab],
+  )
+  const onPressProfile = React.useCallback(() => {
+    track('MobileShell:ProfileButtonPressed')
+    navigation.navigate('Profile', {name: store.me.handle})
+  }, [navigation, track, store.me.handle])
+
+  return (
+    <Animated.View
+      style={[
+        styles.bottomBar,
+        pal.view,
+        pal.border,
+        {paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
+        footerMinimalShellTransform,
+      ]}>
+      <Btn
+        testID="bottomBarHomeBtn"
+        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
+        testID="bottomBarSearchBtn"
+        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
+        testID="bottomBarNotificationsBtn"
+        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 + store.invitedUsers.numNotifs
+        }
+      />
+      <Btn
+        testID="bottomBarProfileBtn"
+        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({
+  testID,
+  icon,
+  notificationCount,
+  onPress,
+  onLongPress,
+}: {
+  testID?: string
+  icon: JSX.Element
+  notificationCount?: number
+  onPress?: (event: GestureResponderEvent) => void
+  onLongPress?: (event: GestureResponderEvent) => void
+}) {
+  return (
+    <TouchableOpacity
+      testID={testID}
+      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>
+  )
+}
diff --git a/src/view/shell/bottom-bar/BottomBarStyles.tsx b/src/view/shell/bottom-bar/BottomBarStyles.tsx
new file mode 100644
index 000000000..3d5adbc9e
--- /dev/null
+++ b/src/view/shell/bottom-bar/BottomBarStyles.tsx
@@ -0,0 +1,61 @@
+import {StyleSheet} from 'react-native'
+import {colors} from 'lib/styles'
+
+export 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: '52%',
+    top: 8,
+    backgroundColor: colors.blue3,
+    paddingHorizontal: 4,
+    paddingBottom: 1,
+    borderRadius: 6,
+    zIndex: 1,
+  },
+  notificationCountLight: {
+    borderColor: colors.white,
+  },
+  notificationCountDark: {
+    borderColor: colors.gray8,
+  },
+  notificationCountLabel: {
+    fontSize: 12,
+    fontWeight: 'bold',
+    color: colors.white,
+    fontVariant: ['tabular-nums'],
+  },
+  ctrlIcon: {
+    marginLeft: 'auto',
+    marginRight: 'auto',
+  },
+  ctrlIconSizingWrapper: {
+    height: 27,
+  },
+  homeIcon: {
+    top: 0,
+  },
+  searchIcon: {
+    top: -2,
+  },
+  bellIcon: {
+    top: -2.5,
+  },
+  profileIcon: {
+    top: -4,
+  },
+})
diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx
new file mode 100644
index 000000000..b7daac5af
--- /dev/null
+++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx
@@ -0,0 +1,101 @@
+import React from 'react'
+import {observer} from 'mobx-react-lite'
+import {useStores} from 'state/index'
+import {usePalette} from 'lib/hooks/usePalette'
+import {Animated} from 'react-native'
+import {useNavigationState} from '@react-navigation/native'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {getCurrentRoute, isTab} from 'lib/routes/helpers'
+import {styles} from './BottomBarStyles'
+import {clamp} from 'lib/numbers'
+import {
+  BellIcon,
+  BellIconSolid,
+  HomeIcon,
+  HomeIconSolid,
+  MagnifyingGlassIcon2,
+  MagnifyingGlassIcon2Solid,
+  UserIcon,
+} from 'lib/icons'
+import {Link} from 'view/com/util/Link'
+import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
+
+export const BottomBarWeb = observer(() => {
+  const store = useStores()
+  const pal = usePalette('default')
+  const safeAreaInsets = useSafeAreaInsets()
+  const {footerMinimalShellTransform} = useMinimalShellMode()
+
+  return (
+    <Animated.View
+      style={[
+        styles.bottomBar,
+        pal.view,
+        pal.border,
+        {paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
+        footerMinimalShellTransform,
+      ]}>
+      <NavItem routeName="Home" href="/">
+        {({isActive}) => {
+          const Icon = isActive ? HomeIconSolid : HomeIcon
+          return (
+            <Icon
+              strokeWidth={4}
+              size={24}
+              style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
+            />
+          )
+        }}
+      </NavItem>
+      <NavItem routeName="Search" href="/search">
+        {({isActive}) => {
+          const Icon = isActive
+            ? MagnifyingGlassIcon2Solid
+            : MagnifyingGlassIcon2
+          return (
+            <Icon
+              size={25}
+              style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
+              strokeWidth={1.8}
+            />
+          )
+        }}
+      </NavItem>
+      <NavItem routeName="Notifications" href="/notifications">
+        {({isActive}) => {
+          const Icon = isActive ? BellIconSolid : BellIcon
+          return (
+            <Icon
+              size={24}
+              strokeWidth={1.9}
+              style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
+            />
+          )
+        }}
+      </NavItem>
+      <NavItem routeName="Profile" href={`/profile/${store.me.handle}`}>
+        {() => (
+          <UserIcon
+            size={28}
+            strokeWidth={1.5}
+            style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
+          />
+        )}
+      </NavItem>
+    </Animated.View>
+  )
+})
+
+const NavItem: React.FC<{
+  children: (props: {isActive: boolean}) => React.ReactChild
+  href: string
+  routeName: string
+}> = ({children, href, routeName}) => {
+  const currentRoute = useNavigationState(getCurrentRoute)
+  const isActive = isTab(currentRoute.name, routeName)
+  return (
+    <Link href={href} style={styles.ctrl}>
+      {children({isActive})}
+    </Link>
+  )
+}