Merge branch 'main' into mob-722-ts

This commit is contained in:
Ryan Stelly
2025-12-12 14:18:02 -06:00
committed by GitHub
120 changed files with 944 additions and 1049 deletions

View File

@@ -148,7 +148,8 @@ module.exports = {
"@typescript-eslint/consistent-type-imports": ["error", {
fixStyle: "separate-type-imports"
}],
"import/consistent-type-specifier-style": ["error", "prefer-top-level"]
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
"@stylistic/member-delimiter-style": "error"
},
ignorePatterns: ["!.detoxrc.js", "/coverage/*", "/vendor/*", "**/flow-typed"],
settings: {
@@ -168,7 +169,8 @@ module.exports = {
"@typescript-eslint/no-wrapper-object-types": "off",
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/consistent-type-imports": "off",
"import/consistent-type-specifier-style": "off"
"import/consistent-type-specifier-style": "off",
"@stylistic/member-delimiter-style": "off"
}
},
{

View File

@@ -13,18 +13,18 @@ export default async function signIn() {
*/
await switchPowerMode();
// Find the Menu item from tabs
const openDrawerMenuItem = element( by.id( "OPEN_DRAWER" ) );
await waitFor( openDrawerMenuItem ).toBeVisible().withTimeout( TIMEOUT );
await expect( openDrawerMenuItem ).toBeVisible();
await element( by.id( "OPEN_DRAWER" ) ).tap( { x: 0, y: 0 } );
const menuButton = element( by.id( "Menu" ) );
await waitFor( menuButton ).toBeVisible().withTimeout( TIMEOUT );
await expect( menuButton ).toBeVisible();
await element( by.id( "Menu" ) ).tap( { x: 0, y: 0 } );
// Tap the Log-In menu item
// TODO: consider this a temporary solution as it only checks for the drawer-top-banner
// TODO: consider this a temporary solution as it only checks for the menu-header
// which can be a login prompt or the logged in user's details. If the user is already
// logged in, this should fail instead.
const loginMenuItem = element( by.id( "drawer-top-banner" ) );
const loginMenuItem = element( by.id( "menu-header" ) );
await waitFor( loginMenuItem ).toBeVisible().withTimeout( TIMEOUT );
await expect( loginMenuItem ).toBeVisible();
await element( by.id( "drawer-top-banner" ) ).tap();
await element( by.id( "menu-header" ) ).tap();
const usernameInput = element( by.id( "Login.email" ) );
await waitFor( usernameInput ).toBeVisible().withTimeout( TIMEOUT );
await expect( usernameInput ).toBeVisible();
@@ -37,7 +37,7 @@ export default async function signIn() {
const loginButton = element( by.id( "Login.loginButton" ) );
await expect( loginButton ).toBeVisible();
await element( by.id( "Login.loginButton" ) ).tap();
const username = element( by.text( `${Config.E2E_TEST_USERNAME}` ) ).atIndex( 1 );
const username = element( by.text( `${Config.E2E_TEST_USERNAME}` ) );
await waitFor( username ).toBeVisible().withTimeout( TIMEOUT );
await expect( username ).toBeVisible();

View File

@@ -5,13 +5,13 @@ import {
const TIMEOUT = 10_000;
export default async function switchPowerMode() {
const drawerButton = element( by.id( "OPEN_DRAWER" ) );
await waitFor( drawerButton ).toBeVisible().withTimeout( TIMEOUT );
await drawerButton.tap( { x: 0, y: 0 } );
// Tap the settings drawer menu item
const settingsDrawerMenuItem = element( by.id( "settings" ) );
await waitFor( settingsDrawerMenuItem ).toBeVisible().withTimeout( TIMEOUT );
await settingsDrawerMenuItem.tap();
const menuButton = element( by.id( "Menu" ) );
await waitFor( menuButton ).toBeVisible().withTimeout( TIMEOUT );
await menuButton.tap( { x: 0, y: 0 } );
// Tap the settings menu item
const settingsMenuItem = element( by.id( "settings" ) );
await waitFor( settingsMenuItem ).toBeVisible().withTimeout( TIMEOUT );
await settingsMenuItem.tap();
// Switch settings to advanced interface mode
const advancedInterfaceSwitch = element( by.id( "advanced-interface-switch.switch" ) );
await waitFor( advancedInterfaceSwitch ).toBeVisible().withTimeout( TIMEOUT );

53
package-lock.json generated
View File

@@ -29,7 +29,6 @@
"@react-native-picker/picker": "^2.11.1",
"@react-native-vector-icons/common": "^12.3.0",
"@react-navigation/bottom-tabs": "^7.4.6",
"@react-navigation/drawer": "^7.5.7",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.17",
"@react-navigation/native-stack": "^7.3.25",
@@ -6413,36 +6412,6 @@
"react": ">=16.8"
}
},
"node_modules/@react-navigation/drawer": {
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/@react-navigation/drawer/-/drawer-7.5.7.tgz",
"integrity": "sha512-iRmeFUMZ4DPYgiuPVKsohL40flGAO0rxWwOg4iWkh0DsglI9yKpDXTUsUjmY4bMTv61jYYWV92OcSCBJjXwJoA==",
"license": "MIT",
"dependencies": {
"@react-navigation/elements": "^2.6.3",
"color": "^4.2.3",
"react-native-drawer-layout": "^4.1.12",
"use-latest-callback": "^0.2.4"
},
"peerDependencies": {
"@react-navigation/native": "^7.1.17",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-gesture-handler": ">= 2.0.0",
"react-native-reanimated": ">= 2.0.0",
"react-native-safe-area-context": ">= 4.0.0",
"react-native-screens": ">= 4.0.0"
}
},
"node_modules/@react-navigation/drawer/node_modules/use-latest-callback": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.4.tgz",
"integrity": "sha512-LS2s2n1usUUnDq4oVh1ca6JFX9uSqUncTfAm44WMg0v6TxL7POUTk1B044NH8TeLkFbNajIsgDHcgNpNzZucdg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/@react-navigation/elements": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.6.3.tgz",
@@ -20146,28 +20115,6 @@
"react-native-reanimated": ">=2.8.0"
}
},
"node_modules/react-native-drawer-layout": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.1.12.tgz",
"integrity": "sha512-oKolvp5seiUieG+RHGjpFe8rH8Ds24iW0QBl31TlCVOX7tdn42IQIBl5tuD1i7h3q+VqqnbcT+NB2dcJ5suZkw==",
"dependencies": {
"use-latest-callback": "^0.2.4"
},
"peerDependencies": {
"react": ">= 18.2.0",
"react-native": "*",
"react-native-gesture-handler": ">= 2.0.0",
"react-native-reanimated": ">= 2.0.0"
}
},
"node_modules/react-native-drawer-layout/node_modules/use-latest-callback": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.4.tgz",
"integrity": "sha512-LS2s2n1usUUnDq4oVh1ca6JFX9uSqUncTfAm44WMg0v6TxL7POUTk1B044NH8TeLkFbNajIsgDHcgNpNzZucdg==",
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-native-event-listeners": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/react-native-event-listeners/-/react-native-event-listeners-1.0.7.tgz",

View File

@@ -65,7 +65,6 @@
"@react-native-picker/picker": "^2.11.1",
"@react-native-vector-icons/common": "^12.3.0",
"@react-navigation/bottom-tabs": "^7.4.6",
"@react-navigation/drawer": "^7.5.7",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.17",
"@react-navigation/native-stack": "^7.3.25",

View File

@@ -66,10 +66,10 @@ export interface ErrorWithResponse {
json: () => Promise<{
status: string;
errors: Array<{
errorCode: string,
message: string,
from: string | null,
stack: string | null,
errorCode: string;
message: string;
from: string | null;
stack: string | null;
}>;
}>;
};

View File

@@ -17,7 +17,7 @@ const api = create( {
}
} );
function isError( error: { message?: string, stack?: string } ) {
function isError( error: { message?: string; stack?: string } ) {
if ( error instanceof Error ) return true;
if ( error?.stack && error?.message ) return true;
return false;

View File

@@ -20,12 +20,12 @@ interface SearchResponse extends ApiResponse {
project?: ApiProject;
taxon?: ApiTaxon;
user?: ApiUser;
}[]
}[];
}
interface SearchParams extends ApiParams {
q?: string;
sources?: string | string[]
sources?: string | string[];
}
const PARAMS: ApiParams = {

2
src/api/types.d.ts vendored
View File

@@ -153,7 +153,7 @@ export interface ApiSuggestion {
}
export interface ApiObservationsSearchResponse extends ApiResponse {
results: ApiObservation[]
results: ApiObservation[];
}
export const ORDER_BY_CREATED_AT = "created_at";

View File

@@ -12,18 +12,18 @@ import colors from "styles/tailwindColors";
interface Props {
closeBottomSheet: ( ) => void;
navAndCloseBottomSheet: ( screen: string, params?: {
camera?: string
camera?: string;
} ) => void;
hidden: boolean;
}
type ObsCreateItem = {
text: string,
icon: string,
onPress: ( ) => void,
testID: string,
accessibilityLabel: string,
accessibilityHint: string
text: string;
icon: string;
onPress: ( ) => void;
testID: string;
accessibilityLabel: string;
accessibilityHint: string;
}
const majorVersionIOS = parseInt( String( Platform.Version ), 10 );

View File

@@ -1,7 +1,7 @@
// @flow
import { useNavigation } from "@react-navigation/native";
import RootDrawerNavigator from "navigation/rootDrawerNavigator";
import RootStackNavigator from "navigation/RootStackNavigator";
import type { Node } from "react";
import React, { useCallback } from "react";
import { log } from "sharedHelpers/logger";
@@ -80,7 +80,7 @@ const App = ( { children }: Props ): Node => {
<StartupService />
<NetworkService />
<AppStateListener />
{children || <RootDrawerNavigator />}
{children || <RootStackNavigator />}
</>
);
};

View File

@@ -231,7 +231,12 @@ const AICamera = ( {
const handleClose = async ( ) => {
await deleteSentinelFile( sentinelFileName );
navigation.goBack( );
navigation.navigate( "TabNavigator", {
screen: "ObservationsTab",
params: {
screen: "ObsList"
}
} );
};
return (

View File

@@ -32,20 +32,20 @@ Reanimated.addWhitelistedNativeProps( {
} );
interface Props {
animatedProps: CameraProps,
cameraRef: React.RefObject<Camera | null>,
cameraScreen: "standard" | "ai",
debugFormat: CameraDeviceFormat | undefined,
device: CameraDevice,
frameProcessor?: () => void,
onCameraError: ( error: CameraRuntimeError ) => void,
onCaptureError: ( error: CameraRuntimeError ) => void,
onClassifierError: ( error: CameraRuntimeError ) => void,
onDeviceNotSupported: ( error: CameraRuntimeError ) => void,
panToZoom: PanGesture,
pinchToZoom: PinchGesture,
resizeMode?: "cover" | "contain",
inactive?: boolean
animatedProps: CameraProps;
cameraRef: React.RefObject<Camera | null>;
cameraScreen: "standard" | "ai";
debugFormat: CameraDeviceFormat | undefined;
device: CameraDevice;
frameProcessor?: () => void;
onCameraError: ( error: CameraRuntimeError ) => void;
onCaptureError: ( error: CameraRuntimeError ) => void;
onClassifierError: ( error: CameraRuntimeError ) => void;
onDeviceNotSupported: ( error: CameraRuntimeError ) => void;
panToZoom: PanGesture;
pinchToZoom: PinchGesture;
resizeMode?: "cover" | "contain";
inactive?: boolean;
}
// A container for the Camera component

View File

@@ -11,23 +11,23 @@ import StandardCamera from "./StandardCamera/StandardCamera";
const isTablet = DeviceInfo.isTablet( );
interface Props {
cameraType: string,
device: CameraDevice,
camera: object,
flipCamera: ( ) => void,
handleCheckmarkPress: ( ) => void,
cameraType: string;
device: CameraDevice;
camera: object;
flipCamera: ( ) => void;
handleCheckmarkPress: ( ) => void;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
toggleFlash: Function,
takingPhoto: boolean,
toggleFlash: Function;
takingPhoto: boolean;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
takePhotoAndStoreUri: Function,
newPhotoUris: Array<object>,
takePhotoAndStoreUri: Function;
newPhotoUris: Array<object>;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
setNewPhotoUris: Function,
takePhotoOptions: object,
userLocation: UserLocation | null,
hasLocationPermissions: boolean,
requestLocationPermissions: () => void,
setNewPhotoUris: Function;
takePhotoOptions: object;
userLocation: UserLocation | null;
hasLocationPermissions: boolean;
requestLocationPermissions: () => void;
}
const CameraWithDevice = ( {

View File

@@ -4,8 +4,8 @@ import { Animated } from "react-native";
import colors from "styles/tailwindColors";
interface Props {
takingPhoto: boolean,
cameraType: string
takingPhoto: boolean;
cameraType: string;
}
const fade = value => ( {

View File

@@ -4,7 +4,7 @@ import React from "react";
import { Animated } from "react-native";
interface Props {
animatedStyle: object
animatedStyle: object;
}
const FocusSquare = ( { animatedStyle }: Props ) => {

View File

@@ -75,7 +75,7 @@ const boldClassname = ( line: string, isDirectory = false ) => classnames(
interface DirectorySizesProps {
directoryName: string;
directoryEntrySizes: DirectoryEntrySize[]
directoryEntrySizes: DirectoryEntrySize[];
}
/* eslint-disable i18next/no-literal-string */

View File

@@ -121,7 +121,7 @@ export function getTotalDirectorySize( directoryItems: DirectoryEntrySize[] ): n
}
type AppSize = {
[directoryName: string]: DirectoryEntrySize[]
[directoryName: string]: DirectoryEntrySize[];
}
async function fetchAppSize(): Promise<AppSize> {

View File

@@ -50,19 +50,19 @@ const centeredLoadingWheel = {
interface Props {
// Bounding box of the observations retrieved for the query params
observationBounds?: MapBoundaries,
observationBounds?: MapBoundaries;
queryParams: {
taxon_id?: number;
return_bounds?: boolean;
order?: string;
orderBy?: string;
};
isLoading: boolean,
hasLocationPermissions?: boolean,
isLoading: boolean;
hasLocationPermissions?: boolean;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
renderLocationPermissionsGate: Function,
renderLocationPermissionsGate: Function;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
requestLocationPermissions: Function
requestLocationPermissions: Function;
}
const MapView = ( {

View File

@@ -21,8 +21,8 @@ const DROP_SHADOW = getShadow( {
} );
type Props = {
closeModal: ( ) => void,
updateProject: ( project: ApiProject ) => void
closeModal: ( ) => void;
updateProject: ( project: ApiProject ) => void;
};
const ExploreProjectSearch = ( { closeModal, updateProject }: Props ) => {

View File

@@ -62,14 +62,14 @@ type FullPageWebViewParams = {
}
type ParamList = {
FullPageWebView: FullPageWebViewParams
FullPageWebView: FullPageWebViewParams;
}
type WebViewSource = {
uri: string;
headers?: {
Authorization?: string | null
}
Authorization?: string | null;
};
}
export function onShouldStartLoadWithRequest(

View File

@@ -187,9 +187,9 @@ const getUsername = async (): Promise<string> => getSensitiveItem( "username" );
*/
const signOut = async (
options: {
realm?: Realm,
clearRealm?: boolean,
queryClient?: QueryClient
realm?: Realm;
clearRealm?: boolean;
queryClient?: QueryClient;
} = {
clearRealm: false,
queryClient: undefined
@@ -579,7 +579,7 @@ async function authenticateUserByAssertion(
}
interface CreateUserResponse {
errors?: string[]
errors?: string[];
}
/**

View File

@@ -15,7 +15,7 @@ import LoginSignUpWrapper from "./LoginSignUpWrapper";
type RenderProps = {
// eslint-disable-next-line react/no-unused-prop-types
scrollViewRef: { current: null | React.Ref<typeof ScrollView> }
scrollViewRef: { current: null | React.Ref<typeof ScrollView> };
};
const ForgotPassword = ( ) => {

View File

@@ -16,8 +16,8 @@ import useKeyboardInfo from "sharedHooks/useKeyboardInfo";
import LoginSignUpInputField from "./LoginSignUpInputField";
type Props = {
reset: ( email: string ) => Promise<void>,
scrollViewRef?: { current: null | ElementRef<typeof ScrollView> },
reset: ( email: string ) => Promise<void>;
scrollViewRef?: { current: null | ElementRef<typeof ScrollView> };
}
const ForgotPasswordForm = ( { reset, scrollViewRef }: Props ): Node => {

View File

@@ -28,7 +28,7 @@ import LoginSignUpInputField from "./LoginSignUpInputField";
const { useRealm } = RealmContext;
interface Props {
scrollViewRef?: React.Ref
scrollViewRef?: React.Ref;
}
interface LoginFormParams {
@@ -38,7 +38,7 @@ interface LoginFormParams {
}
type ParamList = {
LoginFormParams: LoginFormParams
LoginFormParams: LoginFormParams;
}
const LoginForm = ( {

View File

@@ -18,8 +18,8 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import colors from "styles/tailwindColors";
interface Props extends PropsWithChildren {
backgroundSource: ImageSourcePropType,
imageStyle?: StyleProp<ImageStyle>
backgroundSource: ImageSourcePropType;
imageStyle?: StyleProp<ImageStyle>;
}
const windowHeight = Dimensions.get( "window" ).height;

View File

@@ -15,12 +15,12 @@ import {
} from "sharedHooks";
type Props = {
confidence: number | null,
handlePress?: ( ) => void,
taxon: RealmTaxon | ApiTaxon,
testID?: string,
updateMaxHeight?: ( height: number ) => void,
forcedHeight: number
confidence: number | null;
handlePress?: ( ) => void;
taxon: RealmTaxon | ApiTaxon;
testID?: string;
updateMaxHeight?: ( height: number ) => void;
forcedHeight: number;
}
const SuggestionsResult = ( {

View File

@@ -18,7 +18,7 @@ const LIKELY_CONFIDENCE_THRESHOLD = 50;
interface Props {
topSuggestion?: ApiSuggestion;
hideObservationStatus?: boolean
hideObservationStatus?: boolean;
}
const MatchHeader = ( { topSuggestion, hideObservationStatus }: Props ) => {

View File

@@ -62,7 +62,7 @@ const MatchTaxonSearchScreen = ( ) => {
// no-unused-prop-types failing for components defined at runtime seems to
// be a bug. These props are clearly used
// eslint-disable-next-line react/no-unused-prop-types
( { item: taxon, index }: { item: ApiTaxon, index: number } ) => (
( { item: taxon, index }: { item: ApiTaxon; index: number } ) => (
<TaxonResult
accessibilityLabel={t( "Choose-taxon" )}
fetchRemote={false}

View File

@@ -12,9 +12,9 @@ const MIN_SCALE = 0.5;
const MAX_SCALE = 5;
interface Props {
uri: string
setZooming: ( ) => void,
selectedMediaIndex: number
uri: string;
setZooming: ( ) => void;
selectedMediaIndex: number;
}
const CustomImageZoom = ( {

View File

@@ -0,0 +1,282 @@
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, ScrollView, View } from "components/styledComponents";
import { RealmContext } from "providers/contexts";
import React, { useCallback, useState } from "react";
import { Alert } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
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;
}
interface BaseMenuOption {
label: string;
icon: string;
color?: string;
testID?: string;
isLogout?: boolean;
}
interface MenuOptionWithNavigation extends BaseMenuOption {
navigation: string;
onPress?: never;
}
interface MenuOptionWithOnPress extends BaseMenuOption {
onPress: ( ) => void;
navigation?: never;
}
export type MenuOption = MenuOptionWithNavigation | MenuOptionWithOnPress;
export enum MenuModalState {
ConfirmLogout = "confirmLogout",
ProvideFeedback = "provideFeedback"
}
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 insets = useSafeAreaInsets();
const { isConnected } = useNetInfo( );
const [modalState, setModalState] = useState<MenuModalState | null>( null );
const menuItems: Record<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"
},
feedback: {
label: t( "FEEDBACK" ),
icon: "feedback",
onPress: () => {
if ( isConnected ) {
setModalState( MenuModalState.ProvideFeedback );
} else {
showOfflineAlert( t );
}
}
},
...( currentUser
? {
logout: {
label: t( "LOG-OUT" ),
icon: "door-exit",
onPress: () => setModalState( MenuModalState.ConfirmLogout ),
isLogout: true
}
}
: {
login: {
label: t( "LOG-IN" ),
icon: "door-enter",
color: colors.inatGreen,
onPress: () => navigation.navigate( "LoginStackNavigator" )
}
} ),
...( isDebug
? {
debug: {
label: "DEBUG",
navigation: "Debug",
icon: "triangle-exclamation",
color: "deeppink"
}
}
: {} )
};
const onSignOut = async ( ) => {
await signOut( { realm, clearRealm: true, queryClient } );
setModalState( null );
// 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" ) );
setModalState( null );
return true;
}, [isConnected, t] );
return (
<ScrollView
bounces={false}
className="bg-white h-full"
style={{ paddingTop: insets.top }}
>
<View>
{/* Header */}
<Pressable
testID="menu-header"
accessible
accessibilityRole="button"
accessibilityHint={
currentUser
? t( "Navigates-to-user-profile" )
: t( "Navigates-to-log-in-screen" )
}
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>
{modalState === MenuModalState.ConfirmLogout && (
<WarningSheet
onPressClose={() => setModalState( null )}
headerText={t( "LOG-OUT--question" )}
text={t( "Are-you-sure-you-want-to-log-out" )}
handleSecondButtonPress={() => setModalState( null )}
secondButtonText={t( "CANCEL" )}
confirm={onSignOut}
buttonText={t( "LOG-OUT" )}
loading={false}
/>
)}
{modalState === MenuModalState.ProvideFeedback && (
<TextInputSheet
buttonText={t( "SUBMIT" )}
onPressClose={() => setModalState( null )}
headerText={t( "FEEDBACK" )}
confirm={onSubmitFeedback}
description={t( "Thanks-for-using-any-suggestions" )}
maxLength={1000}
/>
)}
</ScrollView>
);
};
export default Menu;

View File

@@ -0,0 +1,34 @@
import classNames from "classnames";
import { Heading4, INatIcon } from "components/SharedComponents";
import { Pressable, View } from "components/styledComponents";
import React from "react";
import type { MenuOption } from "./Menu";
const MenuItem = ( {
item,
onPress
}: {
item: MenuOption;
onPress: ( ) => void;
} ) => (
<Pressable
testID={item.testID}
className={classNames(
item.isLogout
? "opacity-50"
: "",
"flex-row items-center pl-10 py-[22px] 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

@@ -42,8 +42,8 @@ import MyObservationsSimple, {
const { useRealm } = RealmContext;
interface SpeciesCount {
count: number,
taxon: RealmTaxon
count: number;
taxon: RealmTaxon;
}
interface SyncOptions {
@@ -243,9 +243,9 @@ const MyObservationsContainer = ( ): React.FC => {
const onScroll = ( scrollEvent: {
nativeEvent: {
contentOffset: {
y: number
}
}
y: number;
};
};
} ) => setMyObsOffset( scrollEvent.nativeEvent.contentOffset.y );
const numOfUserObservations = zustandStorage.getItem( "numOfUserObservations" );

View File

@@ -38,8 +38,8 @@ import SimpleTaxonGridItem from "./SimpleTaxonGridItem";
import StatTab from "./StatTab";
interface SpeciesCount {
count: number,
taxon: RealmTaxon
count: number;
taxon: RealmTaxon;
}
export interface Props {

View File

@@ -17,15 +17,15 @@ const imageClassNames = [
];
interface SpeciesCount {
count: number,
taxon: RealmTaxon
count: number;
taxon: RealmTaxon;
}
export interface Props {
accessibleName: string;
navToTaxonDetails: ( ) => void;
source: {
uri: string
uri: string;
};
style?: object;
speciesCount: SpeciesCount;

View File

@@ -14,16 +14,16 @@ import { useTranslation } from "sharedHooks";
import type { Notification } from "sharedHooks/useInfiniteNotificationsScroll";
type Props = {
currentUser: RealmUser | null,
data: Notification[],
isError?: boolean,
isFetching?: boolean,
isInitialLoading?: boolean,
isConnected: boolean | null,
onEndReached: ( ) => void,
onRefresh: ( ) => void,
refreshing: boolean,
reload: ( ) => void
currentUser: RealmUser | null;
data: Notification[];
isError?: boolean;
isFetching?: boolean;
isInitialLoading?: boolean;
isConnected: boolean | null;
onEndReached: ( ) => void;
onRefresh: ( ) => void;
refreshing: boolean;
reload: ( ) => void;
};
interface RenderItemProps {

View File

@@ -8,7 +8,7 @@ import type { Notification } from "sharedHooks/useInfiniteNotificationsScroll";
import { OBS_DETAILS_TAB } from "stores/createLayoutSlice";
type Props = {
notification: Notification
notification: Notification;
};
const NotificationsListItem = ( { notification }: Props ) => {

View File

@@ -12,7 +12,7 @@ import type { Notification } from "sharedHooks/useInfiniteNotificationsScroll";
import colors from "styles/tailwindColors";
interface Props {
notification: Notification
notification: Notification;
}
const ObsNotification = ( { notification }: Props ) => {

View File

@@ -8,7 +8,7 @@ import { useTranslation } from "sharedHooks";
import type { Notification } from "sharedHooks/useInfiniteNotificationsScroll";
interface Props {
notification: Notification
notification: Notification;
}
const ObsNotificationText = ( { notification }: Props ) => {

View File

@@ -10,15 +10,15 @@ interface Props {
preferred_common_name?: string;
rank: string;
rank_level: number;
},
username: string,
withdrawn?: boolean
};
username: string;
withdrawn?: boolean;
}
// TODO replace when we've properly typed Realm object
interface User {
prefers_common_names?: boolean;
prefers_scientific_name_first?: boolean
prefers_scientific_name_first?: boolean;
}
const DisagreementText = ( { taxon, username, withdrawn }: Props ) => {

View File

@@ -19,7 +19,7 @@ import DetailsMapHeader from "./DetailsMapHeader";
import ObscurationExplanation from "./ObscurationExplanation";
interface Props {
observation: Observation
observation: Observation;
}
const DETAILS_MAP_MODAL_STYLE = { margin: 0 };

View File

@@ -20,7 +20,7 @@ interface Props {
non_traditional_projects: Array<{
project: object;
}>;
}
};
}
const ProjectSection = ( { observation }: Props ) => {

View File

@@ -12,10 +12,10 @@ import type { RealmTaxon } from "realmModels/types";
import { useCurrentUser, useTranslation } from "sharedHooks";
interface Props {
onPressClose: () => void,
onPressClose: () => void;
onPotentialDisagreePressed: ( _checkedValue: string ) => void;
newTaxon: RealmTaxon | ApiTaxon,
oldTaxon: RealmTaxon | ApiTaxon
newTaxon: RealmTaxon | ApiTaxon;
oldTaxon: RealmTaxon | ApiTaxon;
}
const PotentialDisagreementSheet = ( {

View File

@@ -11,17 +11,17 @@ import type { Node } from "react";
import React from "react";
interface Props {
hidden?: boolean,
hidden?: boolean;
identification: {
body?: string,
taxon: { id: number }
}
body?: string;
taxon: { id: number };
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
onSuggestId:Function,
onSuggestId:Function;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
editIdentBody: Function,
editIdentBody: Function;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
onPressClose: Function
onPressClose: Function;
}
const SuggestIDSheet = ( {

View File

@@ -11,15 +11,15 @@ interface Props {
preferred_common_name?: string;
rank: string;
rank_level: number;
},
username: string,
withdrawn?: boolean
};
username: string;
withdrawn?: boolean;
}
// TODO replace when we've properly typed Realm object
interface User {
prefers_common_names?: boolean;
prefers_scientific_name_first?: boolean
prefers_scientific_name_first?: boolean;
}
const DisagreementText = ( { taxon, username, withdrawn }: Props ) => {

View File

@@ -13,8 +13,8 @@ import { useCurrentUser } from "sharedHooks";
import ObscurationExplanation from "./ObscurationExplanation";
interface Props {
belongsToCurrentUser: boolean,
observation: RealmObservation
belongsToCurrentUser: boolean;
observation: RealmObservation;
}
const LocationSection = ( {

View File

@@ -5,7 +5,7 @@ import React from "react";
import { useCurrentUser } from "sharedHooks";
interface Props {
observationUUID: string
observationUUID: string;
}
const DQAButton = ( { observationUUID }: Props ) => {

View File

@@ -12,7 +12,7 @@ interface Props {
non_traditional_projects: Array<{
project: object;
}>;
}
};
}
const ProjectButton = ( { observation }: Props ) => {

View File

@@ -7,7 +7,7 @@ import React from "react";
import { Alert, Platform, Share } from "react-native";
type Props = {
id: number
id: number;
}
const OBSERVATION_URL = "https://www.inaturalist.org/observations";

View File

@@ -27,22 +27,22 @@ import StatusSection from "./StatusSection/StatusSection";
const cardClassBottom = "rounded-b-2xl border-lightGray border-[2px] pb-3 border-t-0 -mt-0.5 mb-4";
type Props = {
activityItems: Array<object>,
addingActivityItem: boolean,
belongsToCurrentUser: boolean,
currentUser: RealmUser,
isConnected: boolean,
navToSuggestions: () => void,
observation: RealmObservation & Observation & { id: number },
openAddCommentSheet: () => void,
openAgreeWithIdSheet: () => void,
refetchRemoteObservation: () => void,
refetchSubscriptions: () => void,
showAddCommentSheet: () => void,
subscriptions: object,
targetActivityItemID: number,
wasSynced: boolean,
uuid: string
activityItems: Array<object>;
addingActivityItem: boolean;
belongsToCurrentUser: boolean;
currentUser: RealmUser;
isConnected: boolean;
navToSuggestions: () => void;
observation: RealmObservation & Observation & { id: number };
openAddCommentSheet: () => void;
openAgreeWithIdSheet: () => void;
refetchRemoteObservation: () => void;
refetchSubscriptions: () => void;
showAddCommentSheet: () => void;
subscriptions: object;
targetActivityItemID: number;
wasSynced: boolean;
uuid: string;
}
const ObsDetailsDefaultMode = ( {

View File

@@ -13,8 +13,8 @@ import {
import SavedMatchContainer from "./SavedMatch/SavedMatchContainer";
type RouteParams = {
targetActivityItemID?: number,
uuid: string,
targetActivityItemID?: number;
uuid: string;
}
const ObsDetailsDefaultModeScreensWrapper = () => {

View File

@@ -14,8 +14,8 @@ import { useTranslation } from "sharedHooks";
import SavedMatchHeaderRight from "./SavedMatchHeaderRight";
interface Props {
observation: RealmObservation,
navToTaxonDetails: ( ) => void,
observation: RealmObservation;
navToTaxonDetails: ( ) => void;
}
const SavedMatch = ( {

View File

@@ -6,7 +6,7 @@ import React from "react";
import type { RealmObservation } from "realmModels/types";
interface Props {
observation: RealmObservation,
observation: RealmObservation;
}
const SavedMatchContainer = ( { observation }: Props ) => {

View File

@@ -11,17 +11,17 @@ import type { Node } from "react";
import React from "react";
interface Props {
hidden?: boolean,
hidden?: boolean;
identification: {
body?: string,
taxon: { id: number }
}
body?: string;
taxon: { id: number };
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
onSuggestId:Function,
onSuggestId:Function;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
editIdentBody: Function,
editIdentBody: Function;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
onPressClose: Function
onPressClose: Function;
}
const SuggestIDSheet = ( {

View File

@@ -19,15 +19,15 @@ export const SAVE = "save";
export type ButtonType = typeof SAVE | typeof UPLOAD | null;
type Props = {
buttonPressed: ButtonType,
canSaveOnly: boolean,
buttonPressed: ButtonType;
canSaveOnly: boolean;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
handlePress: Function,
loading: boolean,
showFocusedChangesButton: boolean,
showFocusedUploadButton: boolean
showHalfOpacity: boolean,
wasSynced: boolean,
handlePress: Function;
loading: boolean;
showFocusedChangesButton: boolean;
showFocusedUploadButton: boolean;
showHalfOpacity: boolean;
wasSynced: boolean;
}
const BottomButtons = ( {

View File

@@ -21,14 +21,14 @@ import MissingEvidenceSheet from "./Sheets/MissingEvidenceSheet";
const { useRealm } = RealmContext;
type Props = {
passesEvidenceTest: boolean,
observations: Array<object>,
currentObservation: RealmObservation,
currentObservationIndex: number,
passesEvidenceTest: boolean;
observations: Array<object>;
currentObservation: RealmObservation;
currentObservationIndex: number;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
setCurrentObservationIndex: Function,
setCurrentObservationIndex: Function;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
transitionAnimation: Function
transitionAnimation: Function;
}
const BottomButtonsContainer = ( {

View File

@@ -12,9 +12,9 @@ import useTranslation from "sharedHooks/useTranslation";
type Geoprivacy = null | GEOPRIVACY_OPEN | GEOPRIVACY_OBSCURED | GEOPRIVACY_PRIVATE;
type Props = {
onPressClose: ( ) => void,
selectedValue?: Geoprivacy,
updateGeoprivacyStatus: ( Geoprivacy ) => void
onPressClose: ( ) => void;
selectedValue?: Geoprivacy;
updateGeoprivacyStatus: ( Geoprivacy ) => void;
}
const GeoprivacySheet = ( {

View File

@@ -7,21 +7,21 @@ import ObsGridItem from "./ObsGridItem";
import ObsListItem from "./ObsListItem";
type Props = {
currentUser: object,
queued: boolean,
explore: boolean,
hideMetadata?: boolean,
hideObsUploadStatus?: boolean,
hideObsStatus?: boolean,
isSimpleObsStatus?: boolean,
hideRGLabel?: boolean,
onUploadButtonPress: ( ) => void,
onItemPress: ( ) => void,
gridItemStyle: object,
layout: "list" | "grid",
observation: RealmObservation,
uploadProgress: number,
unsynced: boolean
currentUser: object;
queued: boolean;
explore: boolean;
hideMetadata?: boolean;
hideObsUploadStatus?: boolean;
hideObsStatus?: boolean;
isSimpleObsStatus?: boolean;
hideRGLabel?: boolean;
onUploadButtonPress: ( ) => void;
onItemPress: ( ) => void;
gridItemStyle: object;
layout: "list" | "grid";
observation: RealmObservation;
uploadProgress: number;
unsynced: boolean;
};
const ObsPressable = ( {

View File

@@ -6,7 +6,7 @@ import React, { useState } from "react";
import { useCurrentUser, useTranslation } from "sharedHooks";
interface Props {
rule: object
rule: object;
}
const ProjectRuleItem = ( { rule }: Props ) => {

View File

@@ -12,12 +12,12 @@ import {
import ProjectListItem from "./ProjectListItem";
interface Props {
projects: Array<object>
ListEmptyComponent?: React.JSX.Element
ListFooterComponent?: React.JSX.Element
onEndReached?: ( ) => void
onPress?: ( project: ApiProject ) => void
accessibilityLabel?: string
projects: Array<object>;
ListEmptyComponent?: React.JSX.Element;
ListFooterComponent?: React.JSX.Element;
onEndReached?: ( ) => void;
onPress?: ( project: ApiProject ) => void;
accessibilityLabel?: string;
}
const ProjectList = ( {

View File

@@ -34,11 +34,11 @@ interface Props {
isFetchingNextPage: boolean;
isLoading: boolean;
memberId?: number;
projects: object[],
projects: object[];
requestPermissions: () => void;
searchInput: string;
setSearchInput: ( _text: string ) => void;
tabs: Tab[],
tabs: Tab[];
}
const Projects = ( {

View File

@@ -19,15 +19,15 @@ import Animated, {
import colors from "styles/tailwindColors";
type ConfettiProps = PropsWithChildren<{
count: number
duration?: number
count: number;
duration?: number;
}>
type AnimatedElementProps = PropsWithChildren<{
index: number
count: number
animation: SharedValue<number>
duration: number
index: number;
count: number;
animation: SharedValue<number>;
duration: number;
}>
const AnimatedElement = memo(

View File

@@ -15,7 +15,7 @@ interface ButtonProps {
disabled?: boolean;
forceDark?: boolean;
icon?: string;
iconPosition?: string,
iconPosition?: string;
level?: string;
loading?: boolean;
onPress: ( _event?: GestureResponderEvent ) => void;

View File

@@ -23,7 +23,7 @@ interface Props extends PropsWithChildren {
// There is probably a better way to indicate that this tailwind prop is
// supported everywhere, but I haven't found it yet. ~~~kueda 20241016
// eslint-disable-next-line react/no-unused-prop-types
className?: string,
className?: string;
color?: string;
disabled?: boolean;
height?: number;

View File

@@ -5,10 +5,10 @@ import React from "react";
import { useTranslation } from "sharedHooks";
interface Props {
layout?: string,
isConnected?: boolean | null,
hideLoadingWheel: boolean,
explore?: boolean
layout?: string;
isConnected?: boolean | null;
hideLoadingWheel: boolean;
explore?: boolean;
}
const InfiniteScrollLoadingWheel = ( {

View File

@@ -22,20 +22,20 @@ import { getShadow } from "styles/global";
import colors from "styles/tailwindColors";
interface Props {
closeModal: () => void,
coordinateString?: string,
headerTitle?: React.ReactNode,
closeModal: () => void;
coordinateString?: string;
headerTitle?: React.ReactNode;
// TODO MOB-1038: reconcile the type issues here requiring the intersection
observation?: Observation & RealmObservation,
region?: Region,
tileMapParams: Record<string, string> | null,
observation?: Observation & RealmObservation;
region?: Region;
tileMapParams: Record<string, string> | null;
}
interface FloatingActionButtonProps {
accessibilityLabel: string,
buttonClassName: string,
icon: string,
onPress: ( event?: GestureResponderEvent ) => void,
accessibilityLabel: string;
buttonClassName: string;
icon: string;
onPress: ( event?: GestureResponderEvent ) => void;
}
const FloatingActionButton = ( {

View File

@@ -7,11 +7,11 @@ import RNModal from "react-native-modal";
interface Props {
showModal: boolean;
closeModal: () => void;
modal: React.ReactNode,
modal: React.ReactNode;
backdropOpacity?: number;
fullScreen?: boolean;
onModalHide?: () => void,
style?: ViewStyle,
onModalHide?: () => void;
style?: ViewStyle;
animationIn?: string;
animationOut?: string;
disableSwipeDirection?: boolean;

View File

@@ -10,9 +10,9 @@ import React from "react";
import colors from "styles/tailwindColors";
interface Props extends PropsWithChildren {
invertToWhiteBackground: boolean
invertToWhiteBackground: boolean;
headerRight?: React.JSX.Element;
testID: string,
testID: string;
}
const OverlayHeader = ( {

View File

@@ -7,9 +7,9 @@ import * as React from "react";
import colors from "styles/tailwindColors";
interface Props {
qualityGrade: string | null,
color?: string,
opacity?: number
qualityGrade: string | null;
color?: string;
opacity?: number;
}
const qualityGradeSVG = (

View File

@@ -18,7 +18,7 @@ interface Props {
containerClass?: string;
handleTextChange: ( _text: string ) => void;
hasShadow?: boolean;
input?: React.RefObject<RNTextInput | null> | React.MutableRefObject<RNTextInput | undefined>,
input?: React.RefObject<RNTextInput | null> | React.MutableRefObject<RNTextInput | undefined>;
placeholder?: string;
testID?: string;
value: string;

View File

@@ -7,8 +7,8 @@ import {
import React from "react";
type Props = {
props: BottomSheetBackdropProps,
onPress: ( ) => void
props: BottomSheetBackdropProps;
onPress: ( ) => void;
}
const BottomSheetStandardBackdrop = ( { props, onPress }: Props ) => (

View File

@@ -8,25 +8,25 @@ import React, { useState } from "react";
import useTranslation from "sharedHooks/useTranslation";
interface Props {
bottomComponent?: React.JSX.Element
buttonRowClassName?: string
bottomComponent?: React.JSX.Element;
buttonRowClassName?: string;
confirm: ( _checkedValue: string ) => void;
confirmText?: string;
headerText: string,
insideModal?: boolean,
headerText: string;
insideModal?: boolean;
onPressClose?: ( ) => void;
radioValues: {
[key: string]: {
value: string,
icon?: string,
label: string,
text?: string,
buttonText?: string,
}
},
selectedValue?: string,
testID?: string,
topDescriptionText?: React.JSX.Element,
value: string;
icon?: string;
label: string;
text?: string;
buttonText?: string;
};
};
selectedValue?: string;
testID?: string;
topDescriptionText?: React.JSX.Element;
}
const RadioButtonSheet = ( {

View File

@@ -5,7 +5,7 @@ import { useTranslation } from "sharedHooks";
interface Props {
observation: {
private_place_guess?: string
private_place_guess?: string;
};
}

View File

@@ -22,9 +22,9 @@ interface Props {
isLoading?: boolean;
isLocal?: boolean;
renderItem: (
{ item, index }: { item: RealmTaxon, index: number }
{ item, index }: { item: RealmTaxon; index: number }
) => React.ReactElement<unknown>;
taxa: RealmTaxon[]
taxa: RealmTaxon[];
}
const TaxonSearch = ( {

View File

@@ -6,7 +6,7 @@ import colors from "styles/tailwindColors";
const PROGRESS_BAR_STYLE = { backgroundColor: "transparent" };
type Props = {
progress: number
progress: number;
}
const UploadProgressBar = ( { progress }: Props ): Node => (

View File

@@ -4,8 +4,8 @@ import { View } from "components/styledComponents";
import React from "react";
type Props = {
iconClasses: Array<string>,
completeColor: string
iconClasses: Array<string>;
completeColor: string;
}
const UploadCompleteIcon = ( {

View File

@@ -26,7 +26,7 @@ interface Props extends PropsWithChildren {
progress: number;
uniqueKey: string;
queued: boolean;
obsStatus: ReactComponent
obsStatus: ReactComponent;
}
const UploadStatus = ( {

View File

@@ -98,8 +98,8 @@ const LINKIFY_OPTIONS: Opts = {
};
interface Props extends React.PropsWithChildren {
text: string,
htmlStyle?: object,
text: string;
htmlStyle?: object;
}
const UserText = ( {

View File

@@ -57,7 +57,7 @@ export type Suggestion = {
taxon: {
id: number;
name: string;
}
};
};
export type TopSuggestionType = string;

View File

@@ -14,36 +14,36 @@ import Attribution from "./Attribution";
type Props = {
debugData: {
onlineFetchStatus: string,
offlineFetchStatus: string,
selectedPhotoUri: string,
onlineSuggestionsUpdatedAt: Date,
timedOut: boolean,
shouldUseEvidenceLocation: boolean,
topSuggestionType: string,
onlineSuggestions: [],
usingOfflineSuggestions: boolean,
onlineSuggestionsError: Error,
onlineFetchStatus: string;
offlineFetchStatus: string;
selectedPhotoUri: string;
onlineSuggestionsUpdatedAt: Date;
timedOut: boolean;
shouldUseEvidenceLocation: boolean;
topSuggestionType: string;
onlineSuggestions: [];
usingOfflineSuggestions: boolean;
onlineSuggestionsError: Error;
suggestions: {
otherSuggestions: [],
otherSuggestions: [];
topSuggestion: {
taxon: {
id: number,
name: string
},
combined_score: number
}
}
},
id: number;
name: string;
};
combined_score: number;
};
};
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
handleSkip: Function,
handleSkip: Function;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
hideLocationToggleButton: Function,
hideSkip?: boolean,
observers: Array<string>,
shouldUseEvidenceLocation: boolean,
hideLocationToggleButton: Function;
hideSkip?: boolean;
observers: Array<string>;
shouldUseEvidenceLocation: boolean;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
toggleLocation: Function
toggleLocation: Function;
};
const SuggestionsFooter = ( {

View File

@@ -7,10 +7,10 @@ const outputPath = computerVisionPath;
type FlattenUploadArgs = {
image: {
uri: string,
name: string,
type: string
}
uri: string;
name: string;
type: string;
};
}
const flattenUploadParams = async (

View File

@@ -9,7 +9,7 @@ const useNavigateWithTaxonSelected = (
// mysterious background nonsense happening after this screen loses focus
unselectTaxon: () => void,
options: {
vision: boolean
vision: boolean;
}
) => {
const navigation = useNavigation( );

View File

@@ -5,7 +5,7 @@ import React from "react";
interface Props {
commonName: string;
scientificNameFirst?: boolean;
isCurrentTaxon?: boolean
isCurrentTaxon?: boolean;
}
const TaxonomyCommonName = ( {

View File

@@ -9,7 +9,7 @@ import TaxonomyCommonName from "./TaxonomyCommonName";
import TaxonomyScientificName from "./TaxonomyScientificName";
interface Props {
currentUser: { login: string, id: number };
currentUser: { login: string; id: number };
isChild?: boolean;
isCurrentTaxon?: boolean;
navigateToTaxonDetails: ( _taxonId: number ) => void;

View File

@@ -14,15 +14,15 @@ const CONTAINER_STYLE = {
};
interface Props {
ListEmptyComponent?: React.JSX.Element
ListFooterComponent?: React.JSX.Element
onEndReached?: ( ) => void
refreshing?: boolean
users: Array<object>
onPress?: ( ) => void
accessibilityLabel?: string
keyboardShouldPersistTaps?: string
contentContainerStyle?: ViewStyle
ListEmptyComponent?: React.JSX.Element;
ListFooterComponent?: React.JSX.Element;
onEndReached?: ( ) => void;
refreshing?: boolean;
users: Array<object>;
onPress?: ( ) => void;
accessibilityLabel?: string;
keyboardShouldPersistTaps?: string;
contentContainerStyle?: ViewStyle;
}
const UserList = ( {

View File

@@ -8,12 +8,12 @@ import User from "realmModels/User";
import { useTranslation } from "sharedHooks";
interface Props {
item: object
countText: string
item: object;
countText: string;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
onPress?: Function
accessibilityLabel?: string
pressable?: boolean
onPress?: Function;
accessibilityLabel?: string;
pressable?: boolean;
}
const UserListItem = ( {

View File

@@ -751,6 +751,9 @@ Navigates-to-AI-camera = Navigates to AI camera
Navigates-to-bulk-importer = Navigates to bulk importer
Navigates-to-camera = Navigates to camera
Navigates-to-explore = Navigates to explore
Navigates-to-log-in-screen = Navigates to log in screen
# Accessibility hint for the main menu bottom tab
Navigates-to-main-menu = Navigates to main menu.
Navigates-to-match-screen = Navigates to match screen
Navigates-to-notifications = Navigates to notifications
Navigates-to-observation-details = Navigates to observation details screen
@@ -886,7 +889,6 @@ Opens-AI-camera = Opens AI camera.
# Accessibility hint for a button that opens a form for editing a comment
Opens-edit-comment-form = Opens edit comment form.
Opens-location-permission-prompt = Opens location permission prompt
Opens-the-side-drawer-menu = Opens the side drawer menu.
OR-SIGN-IN-WITH = OR SIGN IN WITH
Or-you-can-try-to-get-a-clearer-photo-by-zooming-in-getting-closer = Or, you can try to get a clearer photo by zooming in, getting closer, or trying a different angle.
# Picker prompt on observation edit
@@ -1141,7 +1143,6 @@ Show-observation-options = Show observation options.
Showing-offline-search-results--taxa = Showing offline search results. To search for more species, try again when connected to the Internet.
# Label for button that shows identification suggestions
Shows-identification-suggestions = Shows identification suggestions
Shows-iNaturalist-bird-logo = Shows iNaturalist bird logo.
# Accessibility hint for button that shows observation creation options
Shows-observation-creation-options = Shows observation creation options
# Accessibility label for a button that allows user to sign in with their Apple account

View File

@@ -436,6 +436,8 @@
"Navigates-to-bulk-importer": "Navigates to bulk importer",
"Navigates-to-camera": "Navigates to camera",
"Navigates-to-explore": "Navigates to explore",
"Navigates-to-log-in-screen": "Navigates to log in screen",
"Navigates-to-main-menu": "Navigates to main menu.",
"Navigates-to-match-screen": "Navigates to match screen",
"Navigates-to-notifications": "Navigates to notifications",
"Navigates-to-observation-details": "Navigates to observation details screen",
@@ -516,7 +518,6 @@
"Opens-AI-camera": "Opens AI camera.",
"Opens-edit-comment-form": "Opens edit comment form.",
"Opens-location-permission-prompt": "Opens location permission prompt",
"Opens-the-side-drawer-menu": "Opens the side drawer menu.",
"OR-SIGN-IN-WITH": "OR SIGN IN WITH",
"Or-you-can-try-to-get-a-clearer-photo-by-zooming-in-getting-closer": "Or, you can try to get a clearer photo by zooming in, getting closer, or trying a different angle.",
"Organism-is-captive": "Organism is captive",
@@ -716,7 +717,6 @@
"Show-observation-options": "Show observation options.",
"Showing-offline-search-results--taxa": "Showing offline search results. To search for more species, try again when connected to the Internet.",
"Shows-identification-suggestions": "Shows identification suggestions",
"Shows-iNaturalist-bird-logo": "Shows iNaturalist bird logo.",
"Shows-observation-creation-options": "Shows observation creation options",
"Sign-in-with-Apple": "Sign in with Apple",
"Sign-in-with-Apple-Failed": "Sign in with Apple Failed",

View File

@@ -751,6 +751,9 @@ Navigates-to-AI-camera = Navigates to AI camera
Navigates-to-bulk-importer = Navigates to bulk importer
Navigates-to-camera = Navigates to camera
Navigates-to-explore = Navigates to explore
Navigates-to-log-in-screen = Navigates to log in screen
# Accessibility hint for the main menu bottom tab
Navigates-to-main-menu = Navigates to main menu.
Navigates-to-match-screen = Navigates to match screen
Navigates-to-notifications = Navigates to notifications
Navigates-to-observation-details = Navigates to observation details screen
@@ -886,7 +889,6 @@ Opens-AI-camera = Opens AI camera.
# Accessibility hint for a button that opens a form for editing a comment
Opens-edit-comment-form = Opens edit comment form.
Opens-location-permission-prompt = Opens location permission prompt
Opens-the-side-drawer-menu = Opens the side drawer menu.
OR-SIGN-IN-WITH = OR SIGN IN WITH
Or-you-can-try-to-get-a-clearer-photo-by-zooming-in-getting-closer = Or, you can try to get a clearer photo by zooming in, getting closer, or trying a different angle.
# Picker prompt on observation edit
@@ -1141,7 +1143,6 @@ Show-observation-options = Show observation options.
Showing-offline-search-results--taxa = Showing offline search results. To search for more species, try again when connected to the Internet.
# Label for button that shows identification suggestions
Shows-identification-suggestions = Shows identification suggestions
Shows-iNaturalist-bird-logo = Shows iNaturalist bird logo.
# Accessibility hint for button that shows observation creation options
Shows-observation-creation-options = Shows observation creation options
# Accessibility label for a button that allows user to sign in with their Apple account

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
@@ -11,8 +11,6 @@ import { useCurrentUser, useTranslation } from "sharedHooks";
import CustomTabBar from "./CustomTabBar";
const DRAWER_ID = "OPEN_DRAWER";
interface TabConfig {
icon: string;
testID: string;
@@ -24,11 +22,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 +35,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 +43,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;
}
@@ -57,14 +56,16 @@ const CustomTabBarContainer: React.FC<Props> = ( { navigation, state } ) => {
const tabs: TabConfig[] = useMemo( ( ) => ( [
{
icon: "hamburger-menu",
testID: DRAWER_ID,
testID: SCREEN_NAME_MENU,
accessibilityLabel: t( "Menu" ),
accessibilityHint: t( "Opens-the-side-drawer-menu" ),
accessibilityHint: t( "Navigates-to-main-menu" ),
size: 32,
onPress: ( ) => {
navigation.openDrawer( );
navigation.navigate( "MenuTab", {
screen: "Menu"
} );
},
active: isDrawerOpen
active: SCREEN_NAME_MENU === activeTab
},
{
icon: "magnifying-glass",
@@ -109,7 +110,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
@@ -29,19 +30,25 @@ const BottomTabs = ( ) => {
screenOptions={{
lazy: true,
freezeOnBlur: true,
headerShown: false
headerShown: false,
animation: "fade"
}}
>
<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

@@ -1,348 +0,0 @@
import { useNetInfo } from "@react-native-community/netinfo";
import {
DrawerContentScrollView,
DrawerItem
} from "@react-navigation/drawer";
import { useQueryClient } from "@tanstack/react-query";
import classnames from "classnames";
import {
signOut
} from "components/LoginSignUp/AuthenticationService";
import {
Body1,
Heading4,
INatIcon,
INatIconButton,
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 type { ViewStyle } from "react-native";
import {
Alert, Dimensions
} from "react-native";
import User from "realmModels/User";
import { BREAKPOINTS } from "sharedHelpers/breakpoint";
import { log } from "sharedHelpers/logger";
import { useCurrentUser, useTranslation } from "sharedHooks";
import useStore, { zustandStorage } from "stores/useStore";
import colors from "styles/tailwindColors";
const { useRealm } = RealmContext;
const { width } = Dimensions.get( "screen" );
function isDefaultMode( ) {
return useStore.getState( ).layout.isDefaultMode === true;
}
const createDrawerStyle = ( isDark: boolean ) => ( {
backgroundColor: isDark
? colors.darkModeGray
: "white",
borderTopRightRadius: 20,
borderBottomRightRadius: 20,
minHeight: "100%"
} as const );
interface Props {
state: object;
navigation: object;
descriptors: object;
colorScheme?: string;
}
const feedbackLogger = log.extend( "feedback" );
function showOfflineAlert( t ) {
Alert.alert( t( "You-are-offline" ), t( "Please-try-again-when-you-are-online" ) );
}
const CustomDrawerContent = ( {
state, navigation, descriptors, colorScheme
}: Props ) => {
const isDebug = zustandStorage.getItem( "debugMode" ) === "true";
const isDarkMode = colorScheme === "dark" && isDebug;
const drawerScrollViewStyle = createDrawerStyle( isDarkMode );
const realm = useRealm( );
const queryClient = useQueryClient( );
const currentUser = useCurrentUser( );
const { t } = useTranslation( );
const { isConnected } = useNetInfo( );
const [showConfirm, setShowConfirm] = useState( false );
const [showFeedback, setShowFeedback] = useState( false );
const drawerItemStyle = useMemo( ( ) => ( {
marginBottom: width <= BREAKPOINTS.lg
? -15
: -5
} as const ), [] );
interface DrawerItem {
label: string;
navigation?: string;
icon: string;
color?: string;
style?: ViewStyle;
onPress?: ( ) => void;
testID?: string;
}
const drawerItems = useMemo( ( ) => {
const items: {
[key: string]: DrawerItem;
} = {
projects: {
label: t( "PROJECTS" ),
navigation: "Projects",
icon: "briefcase"
},
about: {
label: t( "ABOUT" ),
navigation: "About",
icon: "inaturalist"
},
donate: {
label: t( "DONATE" ),
navigation: "Donate",
icon: "heart"
},
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",
style: {
opacity: 0.5,
display: "flex"
},
onPress: ( ) => setShowConfirm( true )
};
} else {
items.login = {
label: t( "LOG-IN" ),
icon: "door-enter",
color: colors.inatGreen,
style: {
display: "flex"
},
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 renderIcon = useCallback( ( key: string ) => (
<INatIcon
name={drawerItems[key].icon}
size={22}
color={isDarkMode
? colors.white
: drawerItems[key].color}
/>
), [drawerItems, isDarkMode] );
const renderLabel = useCallback( ( label: string ) => (
<Heading4 className={classnames(
isDarkMode && "dark:text-white"
)}
>
{label}
</Heading4>
), [isDarkMode] );
const renderTopBanner = useCallback( ( ) => (
<Pressable
testID="drawer-top-banner"
accessibilityRole="button"
className={classnames(
currentUser
? "ml-5"
: "ml-3",
"mb-5",
"flex-row",
"flex-nowrap",
"mr-3"
)}
onPress={( ) => {
if ( !currentUser ) {
navigation.navigate( "LoginStackNavigator" );
} else {
navigation.navigate( "TabNavigator", {
screen: "ObservationsTab",
params: {
screen: "UserProfile",
params: { userId: currentUser.id }
}
} );
}
}}
>
{currentUser
? (
<UserIcon
uri={User.uri( currentUser )}
/>
)
: (
<INatIconButton
icon="inaturalist"
size={40}
color={colors.inatGreen}
accessibilityLabel="iNaturalist"
accessibilityHint={t( "Shows-iNaturalist-bird-logo" )}
/>
) }
<View className="ml-3 justify-center">
<Body1 className={classnames(
isDarkMode && "dark:text-white"
)}
>
{currentUser
? currentUser?.login
: t( "Log-in-to-iNaturalist" )}
</Body1>
{currentUser && (
<List2>
{t( "X-Observations", { count: currentUser.observations_count } )}
</List2>
)}
</View>
</Pressable>
), [currentUser, navigation, t, isDarkMode] );
const renderDrawerItem = useCallback( ( key: string ) => (
<View
className="mb-6"
key={drawerItems[key].label}
>
<DrawerItem
testID={drawerItems[key].testID}
accessibilityLabel={drawerItems[key].label}
icon={( ) => renderIcon( key )}
label={() => renderLabel( drawerItems[key].label )}
onPress={( ) => {
if ( drawerItems[key].navigation ) {
navigation.navigate( "TabNavigator", {
screen: "ObservationsTab",
params: {
screen: drawerItems[key].navigation
}
} );
}
if ( drawerItems[key].onPress ) {
drawerItems[key].onPress();
}
}}
style={[drawerItemStyle, drawerItems[key].style]}
/>
</View>
), [
drawerItemStyle,
renderLabel,
renderIcon,
drawerItems,
navigation
] );
const submitFeedback = 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 (
<DrawerContentScrollView
state={state}
navigation={navigation}
descriptors={descriptors}
contentContainerStyle={drawerScrollViewStyle}
>
<View className="py-5 flex">
{renderTopBanner( )}
<View className="ml-3">
{Object.keys( drawerItems ).map( item => renderDrawerItem( item ) )}
</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" )}
/>
)}
{showFeedback && (
<TextInputSheet
hidden={!showFeedback}
buttonText={t( "SUBMIT" )}
onPressClose={() => setShowFeedback( false )}
headerText={t( "FEEDBACK" )}
confirm={submitFeedback}
description={t( "Thanks-for-using-any-suggestions" )}
maxLength={1000}
/>
)}
</DrawerContentScrollView>
);
};
export default CustomDrawerContent;

View File

@@ -5,7 +5,7 @@ import * as React from "react";
import { Animated } from "react-native";
interface Props {
children: React.JSX.Element
children: React.JSX.Element;
}
const FadeInView = ( { children }: Props ) => {

View File

@@ -0,0 +1,48 @@
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import LoginStackNavigator from "navigation/StackNavigators/LoginStackNavigator";
import NoBottomTabStackNavigator from "navigation/StackNavigators/NoBottomTabStackNavigator";
import OnboardingStackNavigator from "navigation/StackNavigators/OnboardingStackNavigator";
import * as React from "react";
import { useOnboardingShown } from "sharedHelpers/installData";
import BottomTabNavigator from "./BottomTabNavigator";
import { hideHeader, preventSwipeToGoBack } from "./navigationOptions";
const Stack = createNativeStackNavigator( );
// DEVELOPERS: do you need to add any screens here? This is the RootStack.
// All the rest of our screens live in:
// NoBottomTabStackNavigator, TabStackNavigator, OnboardingStackNavigator, or LoginStackNavigator
const RootStackNavigator = ( ) => {
const [onboardingShown] = useOnboardingShown( );
return (
<Stack.Navigator screenOptions={{ ...hideHeader, ...preventSwipeToGoBack, animation: "none" }}>
{!onboardingShown
? (
<Stack.Screen
name="OnboardingStackNavigator"
component={OnboardingStackNavigator}
/>
)
: (
<Stack.Screen
name="TabNavigator"
component={BottomTabNavigator}
/>
)}
<Stack.Screen
name="NoBottomTabStackNavigator"
component={NoBottomTabStackNavigator}
/>
<Stack.Screen
name="LoginStackNavigator"
component={LoginStackNavigator}
/>
</Stack.Navigator>
);
};
export default RootStackNavigator;

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,11 +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 FadeInNotifications = ( ) => fadeInComponent( <Notifications /> );
const FadeInRootExplore = ( ) => fadeInComponent( <RootExploreContainer /> );
const FadeInMyObservations = ( ) => fadeInComponent( <MyObservationsContainer /> );
const FadeInUserProfile = ( ) => fadeInComponent( <UserProfile /> );
const FadeInExploreContainer = ( ) => fadeInComponent( <ExploreContainer /> );
const FadeInObsDetailsDefaultModeScreensWrapper = ( ) => fadeInComponent(
<ObsDetailsDefaultModeScreensWrapper />
);
@@ -152,8 +148,7 @@ const NOTIFICATIONS_OPTIONS = {
...preventSwipeToGoBack,
...hideHeaderLeft,
headerTitle: notificationsTitle,
headerTitleAlign: "center",
animation: "none"
headerTitleAlign: "center"
};
const DQA_OPTIONS = {
@@ -180,8 +175,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,9 +198,17 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
<Stack.Group
screenOptions={{ ...hideHeader }}
>
<Stack.Screen
name={SCREEN_NAME_MENU}
component={Menu}
options={{
...preventSwipeToGoBack,
animation: "none"
}}
/>
<Stack.Screen
name={SCREEN_NAME_OBS_LIST}
component={FadeInMyObservations}
component={MyObservationsContainer}
options={{
...preventSwipeToGoBack,
animation: "none"
@@ -212,7 +216,7 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
/>
<Stack.Screen
name={SCREEN_NAME_ROOT_EXPLORE}
component={FadeInRootExplore}
component={RootExploreContainer}
options={{
...preventSwipeToGoBack,
animation: "none"
@@ -220,7 +224,7 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
/>
<Stack.Screen
name="Explore"
component={FadeInExploreContainer}
component={ExploreContainer}
/>
{isDefaultMode
? (
@@ -240,7 +244,7 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
</Stack.Group>
<Stack.Screen
name={SCREEN_NAME_NOTIFICATIONS}
component={FadeInNotifications}
component={Notifications}
options={NOTIFICATIONS_OPTIONS}
/>
<Stack.Screen
@@ -264,7 +268,6 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
name="Projects"
component={FadeInProjectsContainer}
options={{
...isDrawerScreen,
...removeBottomBorder,
...preventSwipeToGoBack
}}
@@ -308,7 +311,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 +377,6 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
name="Settings"
component={FadeInSettings}
options={{
...isDrawerScreen,
headerTitle: settingsTitle
}}
/>
@@ -383,7 +384,6 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
name="About"
component={FadeInAbout}
options={{
...isDrawerScreen,
headerTitle: aboutTitle
}}
/>
@@ -391,7 +391,6 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
name="Donate"
component={FadeInDonate}
options={{
...isDrawerScreen,
headerTitle: donateTitle
}}
/>
@@ -399,7 +398,6 @@ const TabStackNavigator = ( { route }: TabStackNavigatorProps ): Node => {
name="Help"
component={FadeInHelp}
options={{
...isDrawerScreen,
headerTitle: helpTitle
}}
/>

View File

@@ -3,7 +3,6 @@ import { fontMedium } from "appConstants/fontFamilies";
import FullPageWebViewHeader from "components/FullPageWebView/FullPageWebViewHeader";
import BackButton from "components/SharedComponents/Buttons/BackButton";
import React from "react";
import { View } from "react-native";
import colors from "styles/tailwindColors";
import FadeInView from "./FadeInView";
@@ -71,27 +70,14 @@ const removeBottomBorder = {
)
} as const;
// this removes the default hamburger menu from header
const hideDrawerHeaderLeft = {
headerLeft: ( ) => (
<View />
)
} as const;
const preventSwipeToGoBack = {
gestureEnabled: false
} as const;
const isDrawerScreen = {
animation: "none"
} as const;
export {
blankHeaderTitle,
fadeInComponent,
hideDrawerHeaderLeft,
hideHeader,
isDrawerScreen,
preventSwipeToGoBack,
removeBottomBorder,
showHeader,

View File

@@ -1,80 +0,0 @@
// @flow
import { createDrawerNavigator } from "@react-navigation/drawer";
import {
hideDrawerHeaderLeft, hideHeader
} from "navigation/navigationOptions";
import LoginStackNavigator from "navigation/StackNavigators/LoginStackNavigator";
import NoBottomTabStackNavigator from "navigation/StackNavigators/NoBottomTabStackNavigator";
import OnboardingStackNavigator from "navigation/StackNavigators/OnboardingStackNavigator";
import type { Node } from "react";
import * as React from "react";
import { useColorScheme } from "react-native";
import { useOnboardingShown } from "sharedHelpers/installData";
import BottomTabNavigator from "./BottomTabNavigator";
import CustomDrawerContent from "./CustomDrawerContent";
const drawerOptions = {
...hideHeader,
...hideDrawerHeaderLeft,
drawerType: "front",
drawerStyle: {
backgroundColor: "transparent"
},
swipeEnabled: false
};
const Drawer = createDrawerNavigator( );
// DEVELOPERS: do you need to add any screens here? All the rest of our screens live in
// NoBottomTabStackNavigator, TabStackNavigator, OnboardingStackNavigator, or LoginStackNavigator
const RootDrawerNavigator = ( ): Node => {
const [onboardingShown] = useOnboardingShown( );
const colorScheme = useColorScheme( );
const drawerRenderer = ( {
state, navigation, descriptors
} ) => (
<CustomDrawerContent
state={state}
navigation={navigation}
descriptors={descriptors}
colorScheme={colorScheme}
/>
);
return (
<Drawer.Navigator
screenOptions={drawerOptions}
name="Drawer"
drawerContent={drawerRenderer}
>
{!onboardingShown
? (
<Drawer.Screen
name="OnboardingStackNavigator"
component={OnboardingStackNavigator}
/>
)
: (
<Drawer.Screen
name="TabNavigator"
component={BottomTabNavigator}
/>
)}
<Drawer.Screen
name="NoBottomTabStackNavigator"
component={NoBottomTabStackNavigator}
/>
<Drawer.Screen
name="LoginStackNavigator"
component={LoginStackNavigator}
/>
</Drawer.Navigator>
);
};
export default RootDrawerNavigator;

View File

@@ -157,134 +157,134 @@ export interface MapBoundaries {
}
interface PLACE {
display_name: string,
id: number,
place_type: number,
display_name: string;
id: number;
place_type: number;
point_geojson: {
coordinates: Array<number>
},
coordinates: Array<number>;
};
bounding_box_geojson?: {
coordinates: Array<number>
},
type: string
coordinates: Array<number>;
};
type: string;
}
interface DefaultLocation {
placeMode: PLACE_MODE,
lat?: number,
lng?: number,
radius?: number
placeMode: PLACE_MODE;
lat?: number;
lng?: number;
radius?: number;
}
type ExploreProviderProps = {children: React.ReactNode}
type State = {
casual: boolean,
created_d1: string | null | undefined,
created_d2: string | null | undefined,
created_on: string | null | undefined,
d1: string | null | undefined,
d2: string | null | undefined,
dateObserved: DATE_OBSERVED,
dateUploaded: DATE_UPLOADED,
establishmentMean: ESTABLISHMENT_MEAN,
hrank: TAXONOMIC_RANK | undefined | null,
iconic_taxa: string[] | undefined,
lat?: number,
lng?: number,
lrank: TAXONOMIC_RANK | undefined | null,
media: MEDIA,
months: number[] | null | undefined,
needsID: boolean,
nelat?: number,
nelng?: number,
observed_on: string | null | undefined,
photoLicense: PHOTO_LICENSE,
place: PLACE | null | undefined,
place_guess: string,
placeMode: PLACE_MODE,
place_id: number | null | undefined,
casual: boolean;
created_d1: string | null | undefined;
created_d2: string | null | undefined;
created_on: string | null | undefined;
d1: string | null | undefined;
d2: string | null | undefined;
dateObserved: DATE_OBSERVED;
dateUploaded: DATE_UPLOADED;
establishmentMean: ESTABLISHMENT_MEAN;
hrank: TAXONOMIC_RANK | undefined | null;
iconic_taxa: string[] | undefined;
lat?: number;
lng?: number;
lrank: TAXONOMIC_RANK | undefined | null;
media: MEDIA;
months: number[] | null | undefined;
needsID: boolean;
nelat?: number;
nelng?: number;
observed_on: string | null | undefined;
photoLicense: PHOTO_LICENSE;
place: PLACE | null | undefined;
place_guess: string;
placeMode: PLACE_MODE;
place_id: number | null | undefined;
// TODO: technically this is not any object but a "Project"
// and should be typed as such (e.g., in realm model)
project: object | undefined | null,
project_id: number | undefined | null,
radius?: number,
researchGrade: boolean,
return_bounds: boolean | undefined,
reviewedFilter: REVIEWED,
sortBy: SORT_BY,
swlat?: number,
swlng?: number,
project: object | undefined | null;
project_id: number | undefined | null;
radius?: number;
researchGrade: boolean;
return_bounds: boolean | undefined;
reviewedFilter: REVIEWED;
sortBy: SORT_BY;
swlat?: number;
swlng?: number;
// TODO: technically this is not any object but a "Taxon"
// and should be typed as such (e.g., in realm model)
taxon: object | undefined | null,
taxon_id: number | undefined | null,
taxon: object | undefined | null;
taxon_id: number | undefined | null;
// TODO: technically this is not any object but a "User"
// and should be typed as such (e.g., in realm model)
user: object | undefined | null,
user_id: number | undefined | null,
excludeUser: object | undefined | null,
verifiable: boolean,
wildStatus: WILD_STATUS
user: object | undefined | null;
user_id: number | undefined | null;
excludeUser: object | undefined | null;
verifiable: boolean;
wildStatus: WILD_STATUS;
}
type Action = {type: EXPLORE_ACTION.RESET}
| {type: EXPLORE_ACTION.DISCARD, snapshot: State}
| {type: EXPLORE_ACTION.SET_USER, user: object | null, userId: number | null, storedState: State}
| {type: EXPLORE_ACTION.DISCARD; snapshot: State}
| {type: EXPLORE_ACTION.SET_USER; user: object | null; userId: number | null; storedState: State}
| {
type: EXPLORE_ACTION.EXCLUDE_USER,
user: null,
userId: null,
excludeUser: object,
storedState: State
type: EXPLORE_ACTION.EXCLUDE_USER;
user: null;
userId: null;
excludeUser: object;
storedState: State;
}
| {
type: EXPLORE_ACTION.CHANGE_TAXON,
taxon: { id: number } | null,
taxonId: number,
taxonName: string,
storedState?: State
type: EXPLORE_ACTION.CHANGE_TAXON;
taxon: { id: number } | null;
taxonId: number;
taxonName: string;
storedState?: State;
}
| { type: EXPLORE_ACTION.FILTER_BY_ICONIC_TAXON_UNKNOWN }
| {type: EXPLORE_ACTION.SET_EXPLORE_LOCATION, exploreLocation: DefaultLocation}
| {type: EXPLORE_ACTION.SET_EXPLORE_LOCATION; exploreLocation: DefaultLocation}
| {
type: EXPLORE_ACTION.SET_PLACE,
place: PLACE,
placeId: number,
placeGuess?: string,
lat: number,
lng: number,
radius: number,
storedState: State
type: EXPLORE_ACTION.SET_PLACE;
place: PLACE;
placeId: number;
placeGuess?: string;
lat: number;
lng: number;
radius: number;
storedState: State;
}
| {type: EXPLORE_ACTION.SET_PLACE_MODE_NEARBY}
| {type: EXPLORE_ACTION.SET_PLACE_MODE_WORLDWIDE}
| {type: EXPLORE_ACTION.SET_PLACE_MODE_MAP_AREA}
| {type: EXPLORE_ACTION.SET_PLACE_MODE_PLACE}
| {
type: EXPLORE_ACTION.SET_PROJECT,
project: object | null,
projectId: number | null,
storedState: State
type: EXPLORE_ACTION.SET_PROJECT;
project: object | null;
projectId: number | null;
storedState: State;
}
| {type: EXPLORE_ACTION.CHANGE_SORT_BY, sortBy: SORT_BY}
| {type: EXPLORE_ACTION.CHANGE_SORT_BY; sortBy: SORT_BY}
| {type: EXPLORE_ACTION.TOGGLE_RESEARCH_GRADE}
| {type: EXPLORE_ACTION.TOGGLE_NEEDS_ID}
| {type: EXPLORE_ACTION.TOGGLE_CASUAL}
| {type: EXPLORE_ACTION.SET_HIGHEST_TAXONOMIC_RANK, hrank: TAXONOMIC_RANK}
| {type: EXPLORE_ACTION.SET_LOWEST_TAXONOMIC_RANK, lrank: TAXONOMIC_RANK}
| {type: EXPLORE_ACTION.SET_HIGHEST_TAXONOMIC_RANK; hrank: TAXONOMIC_RANK}
| {type: EXPLORE_ACTION.SET_LOWEST_TAXONOMIC_RANK; lrank: TAXONOMIC_RANK}
| {type: EXPLORE_ACTION.SET_DATE_OBSERVED_ALL}
| {type: EXPLORE_ACTION.SET_DATE_OBSERVED_EXACT, observedOn: string}
| {type: EXPLORE_ACTION.SET_DATE_OBSERVED_RANGE, d1: string, d2: string}
| {type: EXPLORE_ACTION.SET_DATE_OBSERVED_MONTHS, months: number[]}
| {type: EXPLORE_ACTION.SET_DATE_OBSERVED_EXACT; observedOn: string}
| {type: EXPLORE_ACTION.SET_DATE_OBSERVED_RANGE; d1: string; d2: string}
| {type: EXPLORE_ACTION.SET_DATE_OBSERVED_MONTHS; months: number[]}
| {type: EXPLORE_ACTION.SET_DATE_UPLOADED_ALL}
| {type: EXPLORE_ACTION.SET_DATE_UPLOADED_EXACT, createdOn: string}
| {type: EXPLORE_ACTION.SET_DATE_UPLOADED_RANGE, createdD1: string, createdD2: string}
| {type: EXPLORE_ACTION.SET_MEDIA, media: MEDIA}
| {type: EXPLORE_ACTION.SET_ESTABLISHMENT_MEAN, establishmentMean: ESTABLISHMENT_MEAN}
| {type: EXPLORE_ACTION.SET_WILD_STATUS, wildStatus: WILD_STATUS}
| {type: EXPLORE_ACTION.SET_REVIEWED, reviewedFilter: REVIEWED}
| {type: EXPLORE_ACTION.SET_PHOTO_LICENSE, photoLicense: PHOTO_LICENSE}
| {type: EXPLORE_ACTION.SET_MAP_BOUNDARIES, mapBoundaries: MapBoundaries}
| {type: EXPLORE_ACTION.USE_STORED_STATE, storedState: State}
| {type: EXPLORE_ACTION.SET_DATE_UPLOADED_EXACT; createdOn: string}
| {type: EXPLORE_ACTION.SET_DATE_UPLOADED_RANGE; createdD1: string; createdD2: string}
| {type: EXPLORE_ACTION.SET_MEDIA; media: MEDIA}
| {type: EXPLORE_ACTION.SET_ESTABLISHMENT_MEAN; establishmentMean: ESTABLISHMENT_MEAN}
| {type: EXPLORE_ACTION.SET_WILD_STATUS; wildStatus: WILD_STATUS}
| {type: EXPLORE_ACTION.SET_REVIEWED; reviewedFilter: REVIEWED}
| {type: EXPLORE_ACTION.SET_PHOTO_LICENSE; photoLicense: PHOTO_LICENSE}
| {type: EXPLORE_ACTION.SET_MAP_BOUNDARIES; mapBoundaries: MapBoundaries}
| {type: EXPLORE_ACTION.USE_STORED_STATE; storedState: State}
type Dispatch = ( action: Action ) => void
const ExploreContext = React.createContext<

View File

@@ -79,8 +79,8 @@ class ObservationPhoto extends Realm.Object {
// I think it is only called after certain transformations on the Realm result,
// but it is not important for my current linear ticket so I'll skip typing it more
static mapObservationPhotoForMyObsDefaultMode( observationPhoto: {
photo?: { url?: string, localFilePath?: string },
uuid?: string
photo?: { url?: string; localFilePath?: string };
uuid?: string;
} ) {
return {
photo: {
@@ -105,7 +105,7 @@ class ObservationPhoto extends Realm.Object {
static createObsPhotosWithPosition = async (
photos: string[] | { image: { uri: string } }[],
{ position, local }: { position: number, local: boolean }
{ position, local }: { position: number; local: boolean }
) => {
let photoPosition = position;
return Promise.all(
@@ -128,7 +128,7 @@ class ObservationPhoto extends Realm.Object {
// linear ticket so I'll skip typing it
static async deleteRemotePhoto(
uri: string,
currentObservation?: { observationPhotos?: { photo: { url?: string }, uuid: string }[] }
currentObservation?: { observationPhotos?: { photo: { url?: string }; uuid: string }[] }
) {
const obsPhotoToDelete = currentObservation?.observationPhotos?.find(
p => p.photo?.url === uri
@@ -155,7 +155,7 @@ class ObservationPhoto extends Realm.Object {
// linear ticket so I'll skip typing it
static async deletePhoto(
uri: string,
currentObservation?: { observationPhotos?: { photo: { url?: string }, uuid: string }[] }
currentObservation?: { observationPhotos?: { photo: { url?: string }; uuid: string }[] }
) {
if ( uri.includes( "https://" ) ) {
ObservationPhoto.deleteRemotePhoto( uri, currentObservation );
@@ -170,8 +170,8 @@ class ObservationPhoto extends Realm.Object {
// linear ticket so I'll skip typing it
static mapObsPhotoUris(
observation: {
observationPhotos?: { photo: RealmPhoto }[],
observation_photos?: { photo: RealmPhoto }[]
observationPhotos?: { photo: RealmPhoto }[];
observation_photos?: { photo: RealmPhoto }[];
}
) {
const obsPhotos = observation?.observationPhotos || observation?.observation_photos;
@@ -190,8 +190,8 @@ class ObservationPhoto extends Realm.Object {
// linear ticket so I'll skip typing it
static mapInnerPhotos(
observation: {
observationPhotos?: { photo: object }[],
observation_photos?: { photo: object }[]
observationPhotos?: { photo: object }[];
observation_photos?: { photo: object }[];
}
) {
const obsPhotos = observation?.observationPhotos || observation?.observation_photos;

View File

@@ -11,14 +11,14 @@ const logger = log.extend( "clearCaches.ts" );
interface RealmObservation {
observationPhotos: {
photo: {
localFilePath: string
}
}[],
localFilePath: string;
};
}[];
observationSounds: {
sound: {
file_url: string
}
}[]
file_url: string;
};
}[];
}
const clearRotatedOriginalPhotosDirectory = async ( ) => {

Some files were not shown because too many files have changed in this diff Show More