add menu screen

This commit is contained in:
Abbey Campbell
2025-12-01 14:18:25 -08:00
parent 8ce00e9fe7
commit b7b1e810da
5 changed files with 321 additions and 19 deletions

View File

@@ -0,0 +1,257 @@
import { useNetInfo } from "@react-native-community/netinfo";
import { useNavigation } from "@react-navigation/native";
import { useQueryClient } from "@tanstack/react-query";
import {
signOut
} from "components/LoginSignUp/AuthenticationService";
import {
Body1,
INatIcon,
List2, TextInputSheet,
UserIcon,
WarningSheet
} from "components/SharedComponents";
import { Pressable, View } from "components/styledComponents";
import { RealmContext } from "providers/contexts";
import React, { useCallback, useMemo, useState } from "react";
import { Alert } from "react-native";
import User from "realmModels/User";
import { log } from "sharedHelpers/logger";
import { useCurrentUser, useTranslation } from "sharedHooks";
import useStore, { zustandStorage } from "stores/useStore";
import colors from "styles/tailwindColors";
import MenuItem from "./MenuItem";
const { useRealm } = RealmContext;
function isDefaultMode( ) {
return useStore.getState( ).layout.isDefaultMode === true;
}
export interface MenuOption {
label: string;
navigation?: string;
icon: string;
color?: string;
onPress?: ( ) => void;
testID?: string;
isLogout?: boolean;
}
const feedbackLogger = log.extend( "feedback" );
function showOfflineAlert( t: ( _: string ) => string ) {
Alert.alert( t( "You-are-offline" ), t( "Please-try-again-when-you-are-online" ) );
}
const Menu = ( ) => {
const isDebug = zustandStorage.getItem( "debugMode" ) === "true";
const realm = useRealm( );
const navigation = useNavigation( );
const queryClient = useQueryClient( );
const currentUser = useCurrentUser( );
const { t } = useTranslation( );
const { isConnected } = useNetInfo( );
const [showConfirm, setShowConfirm] = useState( false );
const [showFeedback, setShowFeedback] = useState( false );
const menuItems = useMemo( ( ) => {
const items: {
[key: string]: MenuOption;
} = {
projects: {
label: t( "PROJECTS" ),
navigation: "Projects",
icon: "briefcase"
},
about: {
label: t( "ABOUT" ),
navigation: "About",
icon: "inaturalist"
},
donate: {
label: t( "DONATE" ),
navigation: "Donate",
icon: "heart",
color: colors.inatGreen
},
help: {
label: t( "HELP" ),
navigation: "Help",
icon: "help-circle"
},
settings: {
testID: "settings",
label: t( "SETTINGS" ),
navigation: "Settings",
icon: "gear"
}
};
items.feedback = {
label: t( "FEEDBACK" ),
icon: "feedback",
onPress: ( ) => {
if ( isConnected ) {
setShowFeedback( true );
} else {
showOfflineAlert( t );
}
}
};
if ( currentUser ) {
items.logout = {
label: t( "LOG-OUT" ),
icon: "door-exit",
onPress: ( ) => setShowConfirm( true ),
isLogout: true
};
} else {
items.login = {
label: t( "LOG-IN" ),
icon: "door-enter",
color: colors.inatGreen,
onPress: ( ) => {
navigation.navigate( "LoginStackNavigator" );
}
};
}
if ( isDebug ) {
items.debug = {
label: "DEBUG",
navigation: "Debug",
icon: "triangle-exclamation",
color: "deeppink"
};
}
return items;
}, [currentUser, isConnected, isDebug, navigation, t] );
const onSignOut = async ( ) => {
await signOut( { realm, clearRealm: true, queryClient } );
setShowConfirm( false );
// TODO might be necessary to restart the app at this point. We just
// deleted the realm file on disk, but the RealmProvider may still have a
// copy of realm in local state
navigation.goBack( );
};
const onSubmitFeedback = useCallback( ( text: string ) => {
if ( !isConnected ) {
showOfflineAlert( t );
return false;
}
const mode = isDefaultMode( )
? "DEFAULT:"
: "ADVANCED:";
feedbackLogger.info( mode, text );
Alert.alert( t( "Feedback-Submitted" ), t( "Thank-you-for-sharing-your-feedback" ) );
setShowFeedback( false );
return true;
}, [isConnected, t] );
return (
<View className="bg-white h-full">
<View>
{/* Header */}
<Pressable
testID="drawer-top-banner" // TO DO: rename test id
accessibilityRole="button"
className="px-[26px] pt-[68px] pb-[31px] border-b border-lightGray"
onPress={( ) => {
if ( !currentUser ) {
navigation.navigate( "LoginStackNavigator" );
} else {
navigation.navigate( "TabNavigator", {
screen: "ObservationsTab",
params: {
screen: "UserProfile",
params: { userId: currentUser.id }
}
} );
}
}}
>
<View className="flex-row">
{currentUser
? (
<UserIcon
uri={User.uri( currentUser )}
medium
/>
)
: (
<INatIcon
name="inaturalist"
color={colors.inatGreen}
size={62}
/>
) }
<View className="ml-5 justify-center">
<Body1>
{currentUser
? currentUser?.login
: t( "Log-in-to-iNaturalist" )}
</Body1>
{currentUser && (
<List2>
{t( "X-Observations", { count: currentUser.observations_count } )}
</List2>
)}
</View>
</View>
</Pressable>
{/* Menu Items */}
<View>
{Object.entries( menuItems ).map( ( [key, item] ) => (
<MenuItem
key={key}
item={item}
onPress={() => {
if ( item.navigation ) {
navigation.navigate( "TabNavigator", {
screen: "MenuTab",
params: {
screen: menuItems[key].navigation
}
} );
}
item.onPress?.();
}}
/>
) )}
</View>
</View>
{showConfirm && (
<WarningSheet
onPressClose={() => setShowConfirm( false )}
headerText={t( "LOG-OUT--question" )}
text={t( "Are-you-sure-you-want-to-log-out" )}
handleSecondButtonPress={() => setShowConfirm( false )}
secondButtonText={t( "CANCEL" )}
confirm={onSignOut}
buttonText={t( "LOG-OUT" )}
loading={false}
/>
)}
{showFeedback && (
<TextInputSheet
buttonText={t( "SUBMIT" )}
onPressClose={() => setShowFeedback( false )}
headerText={t( "FEEDBACK" )}
confirm={onSubmitFeedback}
description={t( "Thanks-for-using-any-suggestions" )}
maxLength={1000}
/>
)}
</View>
);
};
export default Menu;

View File

@@ -0,0 +1,33 @@
import classNames from "classnames";
import { Heading4, INatIcon } from "components/SharedComponents";
import { Pressable, View } from "components/styledComponents";
import React from "react";
import { MenuOption } from "./Menu";
const MenuItem = ( {
item,
onPress
}: {
item: MenuOption;
onPress: ( ) => void;
} ) => (
<Pressable
className={classNames(
item.isLogout
? "opacity-50"
: "",
"flex-row items-center pl-10 py-5 border-b border-lightGray"
)}
accessibilityRole="button"
accessibilityLabel={item.label}
onPress={onPress}
>
<View className="mr-5">
<INatIcon name={item.icon} size={22} color={item.color} />
</View>
<Heading4>{item.label}</Heading4>
</Pressable>
);
export default MenuItem;

View File

@@ -1,6 +1,6 @@
import type { BottomTabBarProps } from "@react-navigation/bottom-tabs";
import { useDrawerStatus } from "@react-navigation/drawer";
import {
SCREEN_NAME_MENU,
SCREEN_NAME_NOTIFICATIONS,
SCREEN_NAME_OBS_LIST,
SCREEN_NAME_ROOT_EXPLORE
@@ -24,11 +24,12 @@ interface TabConfig {
userIconUri?: string;
}
type TabName = "ObservationsTab" | "ExploreTab" | "NotificationsTab";
type TabName = "MenuTab" | "ExploreTab" | "ObservationsTab" | "NotificationsTab";
type ScreenName =
| typeof SCREEN_NAME_OBS_LIST
| typeof SCREEN_NAME_MENU
| typeof SCREEN_NAME_ROOT_EXPLORE
| typeof SCREEN_NAME_OBS_LIST
| typeof SCREEN_NAME_NOTIFICATIONS;
type Props = BottomTabBarProps;
@@ -36,7 +37,6 @@ type Props = BottomTabBarProps;
const CustomTabBarContainer: React.FC<Props> = ( { navigation, state } ) => {
const { t } = useTranslation( );
const currentUser = useCurrentUser( );
const isDrawerOpen = useDrawerStatus() === "open";
const activeTabIndex = state?.index;
const activeTabName = state?.routes[activeTabIndex]?.name as TabName;
@@ -45,8 +45,9 @@ const CustomTabBarContainer: React.FC<Props> = ( { navigation, state } ) => {
const getActiveTab = ( ): ScreenName => {
switch ( activeTabName ) {
case "ObservationsTab": return SCREEN_NAME_OBS_LIST;
case "MenuTab": return SCREEN_NAME_MENU;
case "ExploreTab": return SCREEN_NAME_ROOT_EXPLORE;
case "ObservationsTab": return SCREEN_NAME_OBS_LIST;
case "NotificationsTab": return SCREEN_NAME_NOTIFICATIONS;
default: return SCREEN_NAME_OBS_LIST;
}
@@ -59,12 +60,14 @@ const CustomTabBarContainer: React.FC<Props> = ( { navigation, state } ) => {
icon: "hamburger-menu",
testID: DRAWER_ID,
accessibilityLabel: t( "Menu" ),
accessibilityHint: t( "Opens-the-side-drawer-menu" ),
accessibilityHint: t( "Opens-the-side-drawer-menu" ), // update accessibility hint
size: 32,
onPress: ( ) => {
navigation.openDrawer( );
navigation.navigate( "MenuTab", {
screen: "Menu"
} );
},
active: isDrawerOpen
active: SCREEN_NAME_MENU === activeTab
},
{
icon: "magnifying-glass",
@@ -109,7 +112,6 @@ const CustomTabBarContainer: React.FC<Props> = ( { navigation, state } ) => {
] ), [
activeTab,
userIconUri,
isDrawerOpen,
navigation,
t
] );

View File

@@ -2,6 +2,7 @@ import type { BottomTabBarProps } from "@react-navigation/bottom-tabs";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import Mortal from "components/SharedComponents/Mortal";
import TabStackNavigator, {
SCREEN_NAME_MENU,
SCREEN_NAME_NOTIFICATIONS,
SCREEN_NAME_OBS_LIST,
SCREEN_NAME_ROOT_EXPLORE
@@ -33,15 +34,20 @@ const BottomTabs = ( ) => {
}}
>
<Tab.Screen
name="ObservationsTab"
name="MenuTab"
component={TabStackNavigator}
initialParams={{ initialRouteName: SCREEN_NAME_OBS_LIST }}
initialParams={{ initialRouteName: SCREEN_NAME_MENU }}
/>
<Tab.Screen
name="ExploreTab"
component={TabStackNavigator}
initialParams={{ initialRouteName: SCREEN_NAME_ROOT_EXPLORE }}
/>
<Tab.Screen
name="ObservationsTab"
component={TabStackNavigator}
initialParams={{ initialRouteName: SCREEN_NAME_OBS_LIST }}
/>
<Tab.Screen
name="NotificationsTab"
component={TabStackNavigator}

View File

@@ -15,6 +15,7 @@ import ExploreProjectSearch from "components/Explore/SearchScreens/ExploreProjec
import ExploreTaxonSearch from "components/Explore/SearchScreens/ExploreTaxonSearch";
import ExploreUserSearch from "components/Explore/SearchScreens/ExploreUserSearch";
import Help from "components/Help/Help";
import Menu from "components/Menu/Menu";
import MyObservationsContainer from "components/MyObservations/MyObservationsContainer";
import Notifications from "components/Notifications/Notifications";
import DQAContainer from "components/ObsDetails/DQAContainer";
@@ -38,7 +39,6 @@ import {
fadeInComponent,
hideHeader,
hideHeaderLeft,
isDrawerScreen,
preventSwipeToGoBack,
removeBottomBorder,
showHeader,
@@ -124,6 +124,7 @@ const logTitle = () => <Heading4 className="text-white">LOG</Heading4>;
// note: react navigation 7 will have a layout prop
// which should replace all of these individual wrappers
const FadeInMenu = () => fadeInComponent( <Menu /> );
const FadeInNotifications = ( ) => fadeInComponent( <Notifications /> );
const FadeInRootExplore = ( ) => fadeInComponent( <RootExploreContainer /> );
const FadeInMyObservations = ( ) => fadeInComponent( <MyObservationsContainer /> );
@@ -180,8 +181,9 @@ const OBS_DETAILS_OPTIONS = {
const Stack = createNativeStackNavigator( );
export const SCREEN_NAME_OBS_LIST = "ObsList";
export const SCREEN_NAME_MENU = "Menu";
export const SCREEN_NAME_ROOT_EXPLORE = "RootExplore";
export const SCREEN_NAME_OBS_LIST = "ObsList";
export const SCREEN_NAME_NOTIFICATIONS = "Notifications";
const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
@@ -202,6 +204,14 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
<Stack.Group
screenOptions={{ ...hideHeader }}
>
<Stack.Screen
name={SCREEN_NAME_MENU}
component={FadeInMenu}
options={{
...preventSwipeToGoBack,
animation: "none"
}}
/>
<Stack.Screen
name={SCREEN_NAME_OBS_LIST}
component={FadeInMyObservations}
@@ -264,7 +274,6 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
name="Projects"
component={FadeInProjectsContainer}
options={{
...isDrawerScreen,
...removeBottomBorder,
...preventSwipeToGoBack
}}
@@ -308,7 +317,6 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
{/* Developer Stack Group */}
<Stack.Group
screenOptions={{
...isDrawerScreen,
headerStyle: { backgroundColor: "deeppink", color: "white" },
headerTintColor: "white",
headerTitleStyle: { color: "white" }
@@ -375,7 +383,6 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
name="Settings"
component={FadeInSettings}
options={{
...isDrawerScreen,
headerTitle: settingsTitle
}}
/>
@@ -383,7 +390,6 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
name="About"
component={FadeInAbout}
options={{
...isDrawerScreen,
headerTitle: aboutTitle
}}
/>
@@ -391,7 +397,6 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
name="Donate"
component={FadeInDonate}
options={{
...isDrawerScreen,
headerTitle: donateTitle
}}
/>
@@ -399,7 +404,6 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
name="Help"
component={FadeInHelp}
options={{
...isDrawerScreen,
headerTitle: helpTitle
}}
/>