mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
add menu screen
This commit is contained in:
257
src/components/Menu/Menu.tsx
Normal file
257
src/components/Menu/Menu.tsx
Normal 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;
|
||||
33
src/components/Menu/MenuItem.tsx
Normal file
33
src/components/Menu/MenuItem.tsx
Normal 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;
|
||||
@@ -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
|
||||
] );
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user