Remove unused code and packages, add eslint package for TS, suppress TS errors (#2924)

This commit is contained in:
Amanda Bullington
2025-05-28 17:01:10 -07:00
committed by GitHub
parent 29c0696a88
commit 95151e065b
35 changed files with 818 additions and 2822 deletions

View File

@@ -9,8 +9,6 @@ module.exports = {
},
extends: [
"airbnb",
// This was added to the RN0.72 template, but it does not work with our current setup
// "@react-native",
"plugin:i18next/recommended",
"plugin:@tanstack/eslint-plugin-query/recommended",
"plugin:react-native-a11y/ios",
@@ -74,17 +72,6 @@ module.exports = {
"no-underscore-dangle": 0,
// This gets around eslint problems when typing functions in TS
"no-unused-vars": 0,
"@typescript-eslint/no-unused-vars": [
"error",
{
vars: "all",
args: "after-used",
// Overriding airbnb to allow leading underscore to indicate unused var
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true
}
],
"no-void": 0,
"prefer-destructuring": [2, { object: true, array: false }],
quotes: [2, "double"],
@@ -133,15 +120,34 @@ module.exports = {
"react-native-a11y/has-valid-accessibility-live-region": 1,
"react-native-a11y/has-valid-important-for-accessibility": 1,
"no-shadow": "off",
"@typescript-eslint/no-shadow": "error",
// it's supposedly safe to remove no-undef because TS's compiler handles
// this, but I'm bumping into this error a lot in VSCode - 20240624 amanda
// https://eslint.org/docs/latest/rules/no-undef#handled_by_typescript
"no-undef": "error",
"@typescript-eslint/no-unused-vars": [
"warn",
{
vars: "all",
args: "after-used",
// Overriding airbnb to allow leading underscore to indicate unused var
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true
}
],
// TODO: we should actually type these at some point ~amanda 041824
"@typescript-eslint/no-shadow": "error",
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/no-var-requires": 0
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/no-wrapper-object-types": 0,
"@typescript-eslint/no-require-imports": 0,
"@typescript-eslint/no-unsafe-function-type": 0,
"@typescript-eslint/no-empty-object-types": 0,
"@typescript-eslint/no-empty-object-type": 0,
"@typescript-eslint/no-explicit-any": 1,
"@typescript-eslint/no-unused-expressions": 1
},
// need this so jest doesn't show as undefined in jest.setup.js
env: {

View File

@@ -29,7 +29,6 @@ import { Alert, AppRegistry } from "react-native";
import Config from "react-native-config";
import { setJSExceptionHandler, setNativeExceptionHandler } from "react-native-exception-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { startNetworkLogging } from "react-native-network-logger";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { getInstallID } from "sharedHelpers/installData.ts";
import { reactQueryRetry } from "sharedHelpers/logging";
@@ -113,19 +112,13 @@ setNativeExceptionHandler(
logger.error( `Native Error: ${exceptionString}`, crashData );
} catch ( e ) {
// Last-ditch attempt to log something
logger.error( `Native Error: ${exceptionString} (failed to save context)` );
logger.error( `Native Error: ${exceptionString} (failed to save context)`, e );
}
},
true, // Force quit the app to prevent zombie states
true // Enable on iOS
);
// Only in debug builds
// eslint-disable-next-line no-undef
if ( __DEV__ ) {
startNetworkLogging();
}
initI18next();
// Configure inatjs to use the chosen URLs

1237
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -111,7 +111,6 @@
"react-native-mmkv": "^2.12.2",
"react-native-modal": "^13.0.1",
"react-native-modal-datetime-picker": "^17.1.0",
"react-native-network-logger": "^1.15.0",
"react-native-open-maps": "^0.4.3",
"react-native-orientation-locker": "github:wonday/react-native-orientation-locker",
"react-native-paper": "^5.12.3",
@@ -135,19 +134,16 @@
"react-native-worklets-core": "^1.3.3",
"realm": "^20.1.0",
"sanitize-html": "^2.13.0",
"ts-jest": "^29.1.2",
"uuid": "^11.0.5",
"vision-camera-plugin-inatvision": "github:inaturalist/vision-camera-plugin-inatvision#924a801b224be6243679c8237fbe0ec58ece0ac9",
"zustand": "^4.5.2"
},
"devDependencies": {
"@babel/preset-react": "^7.24.1",
"@babel/preset-typescript": "^7.24.1",
"@babel/runtime": "^7.24.4",
"@faker-js/faker": "^8.4.1",
"@fluent/syntax": "^0.19.0",
"@react-native/babel-preset": "0.73.21",
"@react-native/eslint-config": "0.73.2",
"@react-native/metro-config": "0.73.5",
"@react-native/typescript-config": "0.73.1",
"@tanstack/eslint-plugin-query": "^5.28.11",
@@ -161,6 +157,7 @@
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "^18.0.7",
"@types/sanitize-html": "^2.13.0",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"babel-jest": "^29.6.3",
"babel-plugin-module-resolver": "^5.0.0",
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
@@ -176,6 +173,7 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-module-resolver": "^1.5.0",
"eslint-plugin-react-native": "^5.0.0",
"eslint-plugin-react-native-a11y": "^3.5.1",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-testing-library": "^6.2.0",
@@ -212,4 +210,4 @@
"bundle exec rubocop --force-exclusion --parallel"
]
}
}
}

View File

@@ -1,23 +0,0 @@
// @flow
import inatjs from "inaturalistjs";
import handleError from "./error";
const PARAMS = {
fields: "all"
};
const createFlag = async (
params: Object = {},
opts: Object = {}
): Promise<?Object> => {
try {
const { results } = await inatjs.flags.create( { ...PARAMS, ...params }, opts );
return results;
} catch ( e ) {
return handleError( e, { context: { functionName: "createFlag", opts } } );
}
};
export default createFlag;

View File

@@ -1,16 +0,0 @@
// Wrapper around things that should only be visible in debug mode
import { View } from "components/styledComponents";
import React, { PropsWithChildren } from "react";
import { useDebugMode } from "sharedHooks";
const Debug = ( { children }: PropsWithChildren ) => {
const { isDebug } = useDebugMode( );
if ( !isDebug ) return null;
return (
<View className="bg-deeppink">
{children}
</View>
);
};
export default Debug;

View File

@@ -115,11 +115,6 @@ const Developer = (): Node => {
text="UI LIBRARY"
className="mb-5"
/>
<Button
onPress={() => navigation.navigate( "network" )}
text="NETWORK"
className="mb-5"
/>
<Button
onPress={() => { throw new Error( "Test error" ); }}
text="TEST ERROR"

View File

@@ -1,17 +0,0 @@
import ViewWrapper from "components/SharedComponents/ViewWrapper";
import React from "react";
import NetworkLogger from "react-native-network-logger";
const NetworkLogging = () => {
// eslint-disable-next-line no-undef
if ( !__DEV__ ) {
return null;
}
return (
<ViewWrapper>
<NetworkLogger />
</ViewWrapper>
);
};
export default NetworkLogging;

View File

@@ -1,40 +0,0 @@
// @flow
import { ActivityIndicator } from "components/SharedComponents";
import * as React from "react";
import { FlatList, Text } from "react-native";
type Props = {
loading: boolean,
messageList: Array<Object>,
testID: string
}
const MessageList = ( {
loading,
messageList,
testID
}: Props ): React.Node => {
if ( loading ) {
return (
<ActivityIndicator
testID="Messages.activityIndicator"
size={25}
/>
);
}
const renderMessages = ( { item } ) => (
<Text>{item.subject}</Text>
);
return (
<FlatList
data={messageList}
renderItem={renderMessages}
testID={testID}
/>
);
};
export default MessageList;

View File

@@ -1,66 +0,0 @@
// @flow
import searchMessages from "api/messages.ts";
import { Body3, Tabs, ViewWrapper } from "components/SharedComponents";
import { t } from "i18next";
import type { Node } from "react";
import React, { useState } from "react";
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
import useCurrentUser from "sharedHooks/useCurrentUser.ts";
import MessageList from "./MessageList";
const NOTIFICATIONS_ID = "NOTIFICATIONS";
const MESSAGES_ID = "MESSAGES";
const Messages = (): Node => {
const currentUser = useCurrentUser();
const [activeTab, setActiveTab] = useState( NOTIFICATIONS_ID );
const { data, isLoading } = useAuthenticatedQuery(
["searchMessages"],
optsWithAuth => searchMessages( { page: 1 }, optsWithAuth ),
{
enabled: !!currentUser
}
);
const tabs = [
{
id: NOTIFICATIONS_ID,
text: "Notifications",
onPress: () => {
setActiveTab( NOTIFICATIONS_ID );
}
},
{
id: MESSAGES_ID,
text: "Messages",
onPress: () => {
setActiveTab( MESSAGES_ID );
}
}
];
return (
<ViewWrapper>
{currentUser
? (
<>
<Tabs tabs={tabs} activeId={activeTab} />
<MessageList
loading={isLoading}
messageList={data}
testID="Messages.messages"
/>
</>
)
: (
<Body3 className="self-center">
{t( "You-must-be-logged-in-to-view-messages" )}
</Body3>
)}
</ViewWrapper>
);
};
export default Messages;

View File

@@ -1,182 +0,0 @@
// @flow
import createFlag from "api/flags";
import {
BackButton,
Body1, Body3, Button, Checkbox, Subheading1
} from "components/SharedComponents";
import {
Modal,
SafeAreaView,
View
} from "components/styledComponents";
import { t } from "i18next";
import type { Node } from "react";
import React, { useRef, useState } from "react";
import {
Alert,
findNodeHandle
} from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { TextInput } from "react-native-paper";
import useAuthenticatedMutation from "sharedHooks/useAuthenticatedMutation";
type Props = {
id:number,
itemType:string,
showFlagItemModal: boolean,
closeFlagItemModal: Function,
onItemFlagged: Function
}
const FlagItemModal = ( {
id, itemType, showFlagItemModal, closeFlagItemModal, onItemFlagged
}: Props ): Node => {
const keyboardScrollRef = useRef( null );
const [checkBoxValue, setCheckBoxValue] = useState( "none" );
const [explanation, setExplanation] = useState( "" );
const [loading, setLoading] = useState( false );
const showErrorAlert = error => Alert.alert(
"Error",
error,
[{ text: t( "OK" ) }],
{
cancelable: true
}
);
const scrollToInput = node => {
keyboardScrollRef?.current?.scrollToFocusedInput( node );
};
const toggleCheckBoxValue = checkbox => {
if ( checkBoxValue === checkbox ) {
setCheckBoxValue( "none" );
} else { setCheckBoxValue( checkbox ); }
};
const resetFlagModal = () => {
setCheckBoxValue( "none" );
setExplanation( "" );
closeFlagItemModal();
setLoading( false );
};
const createFlagMutation = useAuthenticatedMutation(
( params, optsWithAuth ) => createFlag( params, optsWithAuth ),
{
onSuccess: data => {
resetFlagModal();
onItemFlagged( data );
},
onError: error => {
setLoading( false );
showErrorAlert( error );
}
}
);
const submitFlag = () => {
if ( checkBoxValue !== "none" ) {
let params = {
flag: {
flaggable_type: itemType,
flaggable_id: id,
flag: checkBoxValue
}
};
if ( checkBoxValue === "other" ) {
params = { ...params, flag_explanation: explanation };
}
setLoading( true );
createFlagMutation.mutate( params );
}
};
return (
<Modal
visible={showFlagItemModal}
animationType="slide"
className="flex-1"
testID="FlagItemModal"
>
<SafeAreaView className="flex-1">
<View className="flex-row-reverse justify-between p-6 border-b">
<BackButton onPress={closeFlagItemModal} />
<Subheading1 className="text-xl">
{t( "Flag-An-Item" )}
</Subheading1>
</View>
<KeyboardAwareScrollView
ref={keyboardScrollRef}
enableOnAndroid
enableAutomaticScroll
extraHeight={200}
className="p-6"
>
<Body1 className="text-base">
{t( "Flag-Item-Description" )}
</Body1>
<View className="flex-row my-2">
<Checkbox
isChecked={checkBoxValue === "spam"}
onValueChange={() => toggleCheckBoxValue( "spam" )}
text={t( "Spam" )}
/>
</View>
<Body1 className="mb-2 text-base" style>{t( "Spam-Examples" )}</Body1>
<View className="flex-row my-2">
<Checkbox
isChecked={checkBoxValue === "inappropriate"}
onValueChange={() => toggleCheckBoxValue( "inappropriate" )}
text={t( "Offensive-Inappropriate" )}
/>
</View>
<Body1 className="mb-2 text-base">{t( "Offensive-Inappropriate-Examples" )}</Body1>
<View className="flex-row my-2">
<Checkbox
isChecked={checkBoxValue === "other"}
onValueChange={() => toggleCheckBoxValue( "other" )}
text={t( "Other" )}
/>
</View>
<Body1 className="mb-2 text-base">{t( "Flag-Item-Other-Description" )}</Body1>
{( checkBoxValue === "other" )
&& (
<>
<TextInput
className="text-sm"
placeholder={t( "Flag-Item-Other-Input-Hint" )}
value={explanation}
onChangeText={text => setExplanation( text )}
onFocus={e => scrollToInput( findNodeHandle( e.target ) )}
accessibilityLabel={t( "Reason--flag" )}
/>
<Body3>{`${explanation.length}/255`}</Body3>
</>
)}
<View className="flex-row justify-center m-4">
<Button
className="m-2"
text={t( "Cancel" )}
onPress={() => resetFlagModal()}
/>
<Button
className="m-2"
text={t( "Save" )}
onPress={submitFlag}
level="primary"
loading={loading}
/>
</View>
</KeyboardAwareScrollView>
</SafeAreaView>
</Modal>
);
};
export default FlagItemModal;

View File

@@ -1,115 +0,0 @@
// @flow
import { createSubscription } from "api/observations";
import KebabMenu from "components/SharedComponents/KebabMenu.tsx";
import { t } from "i18next";
import type { Node } from "react";
import React, { useState } from "react";
import { Alert, Platform, Share } from "react-native";
import {
useAuthenticatedMutation,
useCurrentUser
} from "sharedHooks";
const observationsUrl = "https://www.inaturalist.org/observations";
type Props = {
observationId: number,
white?: boolean,
subscriptions: Object,
uuid: string,
refetchSubscriptions: Function
}
const HeaderKebabMenu = ( {
observationId,
white = true,
subscriptions,
uuid,
refetchSubscriptions
}: Props ): Node => {
const currentUser = useCurrentUser( );
const [kebabMenuVisible, setKebabMenuVisible] = useState( false );
const url = `${observationsUrl}/${observationId?.toString( )}`;
const sharingOptions = {
url: "",
message: ""
};
const isSubscribed = subscriptions?.length > 0;
if ( Platform.OS === "ios" ) {
sharingOptions.url = url;
} else {
sharingOptions.message = url;
}
const handleShare = async ( ) => {
setKebabMenuVisible( false );
try {
return await Share.share( sharingOptions );
} catch ( error ) {
Alert.alert( error.message );
return null;
}
};
const toggleSubscription = useAuthenticatedMutation(
( params, optsWithAuth ) => createSubscription( params, optsWithAuth ),
{
onSuccess: () => {
refetchSubscriptions();
},
onError: error => {
Alert.alert( error.message );
}
}
);
const toggleSubscriptionOnPress = async ( ) => {
setKebabMenuVisible( false );
try {
return await toggleSubscription.mutate( { uuid } );
} catch ( error ) {
Alert.alert( error.message );
return null;
}
};
return (
<KebabMenu
visible={kebabMenuVisible}
setVisible={setKebabMenuVisible}
white={white}
accessibilityLabel={t( "Observation-options" )}
accessibilityHint={t( "Show-observation-options" )}
large
>
<KebabMenu.Item
isFirst
onPress={handleShare}
title={t( "Share" )}
testID="MenuItem.Share"
/>
{!!currentUser && ( isSubscribed
? (
<KebabMenu.Item
isFirst
onPress={toggleSubscriptionOnPress}
title={t( "Ignore-notifications" )}
testID="MenuItem.IgnoreNotifications"
/>
)
: (
<KebabMenu.Item
isFirst
onPress={toggleSubscriptionOnPress}
title={t( "Enable-notifications" )}
testID="MenuItem.EnableNotifications"
/>
) )}
</KebabMenu>
);
};
export default HeaderKebabMenu;

View File

@@ -1,96 +0,0 @@
// @flow
import KebabMenu from "components/SharedComponents/KebabMenu.tsx";
import {
View
} from "components/styledComponents";
import { t } from "i18next";
import type { Node } from "react";
import React, { useState } from "react";
type Props = {
current: boolean,
currentUser: boolean,
itemType: "Identification" | "Comment",
setShowDeleteCommentSheet: Function,
setShowEditCommentSheet: Function,
setShowWithdrawIDSheet: Function,
updateIdentification: Function,
}
const ActivityItemKebabMenu = ( {
current,
currentUser,
itemType,
setShowDeleteCommentSheet,
setShowEditCommentSheet,
setShowWithdrawIDSheet,
updateIdentification
}:Props ): Node => {
const [kebabMenuVisible, setKebabMenuVisible] = useState( false );
if ( !currentUser ) {
// flags removed from mvp
// placeholder for kebabmenu
return <View className="h-[44px] mr-[15px]" />;
}
if ( itemType === "Identification" ) {
return (
<KebabMenu
visible={kebabMenuVisible}
setVisible={setKebabMenuVisible}
accessibilityLabel={t( "Identification-options" )}
>
{current === true
? (
<KebabMenu.Item
isFirst
onPress={async ( ) => {
setShowWithdrawIDSheet( true );
setKebabMenuVisible( false );
}}
title={t( "Withdraw" )}
testID="MenuItem.Withdraw"
/>
)
: (
<KebabMenu.Item
isFirst
onPress={async ( ) => {
updateIdentification( { current: true } );
setKebabMenuVisible( false );
}}
title={t( "Restore" )}
/>
)}
</KebabMenu>
);
}
return (
<KebabMenu
visible={kebabMenuVisible}
setVisible={setKebabMenuVisible}
accessibilityLabel={t( "Comment-options" )}
>
<KebabMenu.Item
onPress={async ( ) => {
setShowEditCommentSheet( true );
setKebabMenuVisible( false );
}}
title={t( "Edit-comment" )}
testID="MenuItem.EditComment"
/>
<KebabMenu.Item
onPress={async ( ) => {
setShowDeleteCommentSheet( true );
setKebabMenuVisible( false );
}}
title={t( "Delete-comment" )}
testID="MenuItem.DeleteComment"
/>
</KebabMenu>
);
};
export default ActivityItemKebabMenu;

View File

@@ -1,315 +0,0 @@
// @flow
import {
refresh as refreshNetInfo,
useNetInfo
} from "@react-native-community/netinfo";
import { useRoute } from "@react-navigation/native";
import { faveObservation, unfaveObservation } from "api/observations";
import { deleteQualityMetric, fetchQualityMetrics, setQualityMetric } from "api/qualityMetrics";
import DataQualityAssessment from "components/ObsDetails/DataQualityAssessment";
import {
BottomSheet,
Button,
List2
} from "components/SharedComponents";
import { View } from "components/styledComponents";
import { t } from "i18next";
import { compact, groupBy } from "lodash";
import { useCallback, useEffect, useState } from "react";
import * as React from "react";
import Observation from "realmModels/Observation";
import { log } from "sharedHelpers/logger";
import {
useAuthenticatedMutation,
useAuthenticatedQuery,
useLocalObservation
} from "sharedHooks";
import useRemoteObservation from "sharedHooks/useRemoteObservation";
const logger = log.extend( "DQAContainer" );
const DQAContainer = ( ): React.Node => {
const { isConnected } = useNetInfo( );
const { params } = useRoute( );
const { observationUUID } = params;
const [loadingAgree, setLoadingAgree] = useState( false );
const [loadingDisagree, setLoadingDisagree] = useState( false );
const [loadingMetric, setLoadingMetric] = useState( "none" );
const [hideErrorSheet, setHideErrorSheet] = useState( true );
const [hideOfflineSheet, setHideOfflineSheet] = useState( true );
const localObservation = useLocalObservation( observationUUID );
const fetchRemoteObservationEnabled = !localObservation || localObservation?.wasSynced();
const {
remoteObservation,
refetchRemoteObservation,
isRefetching
} = useRemoteObservation( observationUUID, fetchRemoteObservationEnabled );
const observation = remoteObservation
? Observation.mapApiToRealm( remoteObservation )
: localObservation;
const fetchMetricsParams = {
id: observationUUID,
fields: "metric,agree,user_id",
ttl: -1
};
const setNotLoading = useCallback( () => {
setLoadingMetric( "none" );
if ( loadingAgree ) {
setLoadingAgree( false );
}
if ( loadingDisagree ) {
setLoadingDisagree( false );
}
}, [loadingAgree, loadingDisagree] );
const setLoading = ( metric, vote ) => {
setLoadingMetric( metric );
if ( vote ) {
setLoadingAgree( true );
} else {
setLoadingDisagree( true );
}
};
const {
data: qualityMetrics,
refetch: refetchQualityMetrics
} = useAuthenticatedQuery(
["fetchQualityMetrics", observationUUID],
optsWithAuth => fetchQualityMetrics( fetchMetricsParams, optsWithAuth )
);
const combinedQualityMetrics = {
...groupBy( qualityMetrics, "metric" ),
...groupBy( observation?.votes, "vote_scope" )
};
/**
* After a success mutation of the needs_id vote we start the refetching of the remote
* observation to update the metric status of the observation. So we need to wait until
* the refetching is done to set the loading state to false.
*/
useEffect( () => {
if ( !isRefetching ) {
setNotLoading();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isRefetching] );
const faveMutation = useAuthenticatedMutation(
( faveParams, optsWithAuth ) => faveObservation( faveParams, optsWithAuth ),
{
onSuccess: () => {
refetchRemoteObservation();
},
onError: () => {
setHideErrorSheet( false );
}
}
);
const unfaveMutation = useAuthenticatedMutation(
( faveParams, optsWithAuth ) => unfaveObservation( faveParams, optsWithAuth ),
{
onSuccess: () => {
refetchRemoteObservation();
},
onError: () => {
setHideErrorSheet( false );
}
}
);
const createQualityMetricMutation = useAuthenticatedMutation(
( qualityMetricParams, optsWithAuth ) => setQualityMetric( qualityMetricParams, optsWithAuth ),
{
onSuccess: async ( ) => {
await refetchQualityMetrics( );
await refetchRemoteObservation( );
setNotLoading( );
},
onError: error => {
logger.error( "createQualityMetricMutation failure", error );
setHideErrorSheet( false );
}
}
);
const setMetricVote = ( { metric, vote } ) => {
setLoading( metric, vote );
const qualityMetricParams = {
id: observationUUID,
metric,
agree: vote,
ttyl: -1
};
createQualityMetricMutation.mutate( qualityMetricParams );
};
// The quality metric "needs_id" uses a fave/unfave vote with vote_scope: "needs_id"
// as it's interaction with the API
const setNeedsIDVote = ( { vote } ) => {
setLoading( "needs_id", vote );
const faveParams = {
id: observationUUID,
scope: "needs_id",
vote: vote === false
? "no"
: "yes"
};
faveMutation.mutate( faveParams );
};
const removeQualityMetricMutation = useAuthenticatedMutation(
( deleteParams, options ) => deleteQualityMetric( deleteParams, options ),
{
onSuccess: async ( ) => {
await refetchQualityMetrics( );
await refetchRemoteObservation( );
setNotLoading( );
},
onError: error => {
logger.error( "removeQualityMetricMutation failed", error );
setHideErrorSheet( false );
}
}
);
const removeMetricVote = ( { metric, vote } ) => {
setLoading( metric, vote );
const qualityMetricParams = {
id: observationUUID,
metric,
ttyl: -1
};
removeQualityMetricMutation.mutate( qualityMetricParams );
};
// The quality metric "needs_id" uses a fave/unfave vote with vote_scope: "needs_id"
// as it's interaction with the API
const removeNeedsIDVote = ( { vote } ) => {
setLoading( "needs_id", vote );
const unfaveParams = {
id: observationUUID,
scope: "needs_id"
};
unfaveMutation.mutate( unfaveParams );
};
const ifMajorityAgree = metric => {
const votesOfMetric = combinedQualityMetrics[metric];
if ( votesOfMetric && votesOfMetric.length > 0 ) {
const agreeCount = votesOfMetric.filter(
element => element.agree
).length;
const disagreeCount = votesOfMetric.filter(
element => !element.agree
).length;
return agreeCount >= disagreeCount;
}
return null;
};
const checkTest = metric => {
if ( observation ) {
const obsDataToCheck = {
date: observation.observed_on,
location: [observation.latitude, observation.longitude],
evidence: compact( [
observation.observationPhotos || observation.observation_photos,
observation.observationSounds || observation.sounds
] ),
taxonId: observation.taxon?.id,
rankLevel: observation.taxon?.rank_level,
identifications: observation.identifications
};
if ( metric === "date" ) {
return obsDataToCheck[metric] !== null;
}
if ( metric === "location" ) {
const removedNull = obsDataToCheck[metric]
.filter( value => value !== null );
return removedNull.length !== 0;
}
if ( metric === "evidence" ) {
const removedEmpty = obsDataToCheck[metric]
.filter( value => Object.keys( value ).length !== 0 );
return removedEmpty.length !== 0;
}
if ( metric === "id_supported" ) {
const { taxonId } = obsDataToCheck;
const supportedIDs = obsDataToCheck.identifications.filter(
identification => identification.taxon.id === taxonId
).length;
return supportedIDs >= 2;
}
if ( metric === "rank" && obsDataToCheck.rankLevel <= 10 ) {
return true;
}
}
return false;
};
const resetButtonsOnError = () => {
setNotLoading();
setHideErrorSheet( true );
};
return (
<>
<DataQualityAssessment
qualityMetrics={combinedQualityMetrics}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
qualityGrade={observation?.quality_grade}
setMetricVote={setMetricVote}
removeMetricVote={removeMetricVote}
setNeedsIDVote={setNeedsIDVote}
removeNeedsIDVote={removeNeedsIDVote}
ifMajorityAgree={ifMajorityAgree}
checkTest={checkTest}
isConnected={isConnected}
recheckisConnected={refreshNetInfo}
/>
<BottomSheet
headerText={t( "ERROR-VOTING-IN-DQA" )}
hidden={hideErrorSheet}
hideCloseButton
onPressClose={( ) => resetButtonsOnError( )}
>
<View className="px-[26px] py-[20px] flex-col space-y-[20px]">
<List2 className="text-darkGray">{t( "Error-voting-in-DQA-description" )}</List2>
<Button
text={t( "OK" )}
onPress={() => resetButtonsOnError( )}
/>
</View>
</BottomSheet>
<BottomSheet
headerText={t( "ERROR-LOADING-DQA" )}
hidden={hideOfflineSheet}
hideCloseButton
onPressClose={( ) => setHideOfflineSheet( true )}
>
<View className="px-[26px] py-[20px] flex-col space-y-[20px]">
<List2 className="text-darkGray">{t( "Offline-DQA-description" )}</List2>
<Button
text={t( "OK" )}
onPress={() => setHideOfflineSheet( true )}
/>
</View>
</BottomSheet>
</>
);
};
export default DQAContainer;

View File

@@ -1,317 +0,0 @@
// @flow
import DQAVoteButtons from "components/ObsDetails/DetailsTab/DQAVoteButtons";
import {
Body1,
Body3,
Divider,
Heading4,
INatIcon,
List2,
OfflineNotice,
ScrollViewWrapper,
ViewWrapper
} from "components/SharedComponents";
// eslint-disable-next-line max-len
import QualityGradeStatus from "components/SharedComponents/QualityGradeStatus/QualityGradeStatus.tsx";
import { View } from "components/styledComponents";
import { t } from "i18next";
import type { Node } from "react";
import React from "react";
import colors from "styles/tailwindColors";
const titleOption = option => {
switch ( option ) {
case "research":
return t( "Data-quality-assessment-title-research" );
case "needs_id":
return t( "Data-quality-assessment-title-needs-id" );
default:
return t( "Data-quality-assessment-title-casual" );
}
};
const titleDescription = option => {
switch ( option ) {
case "research":
return t( "Data-quality-assessment-description-research" );
case "needs_id":
return t( "Data-quality-assessment-description-needs-id" );
default:
return t( "Data-quality-assessment-description-casual" );
}
};
type Props = {
checkTest: Function,
ifMajorityAgree: Function,
isConnected?: boolean,
loadingAgree: boolean,
loadingDisagree: boolean,
loadingMetric: string,
qualityGrade: string,
qualityMetrics: Object,
recheckisConnected: Function,
removeMetricVote: Function,
removeNeedsIDVote: Function,
setMetricVote: Function,
setNeedsIDVote: Function,
}
const DataQualityAssessment = ( {
checkTest,
ifMajorityAgree,
isConnected,
loadingAgree,
loadingDisagree,
loadingMetric,
qualityGrade,
qualityMetrics,
recheckisConnected,
removeMetricVote,
removeNeedsIDVote,
setMetricVote,
setNeedsIDVote
}: Props ): Node => {
const isResearchGrade = qualityGrade === "research";
const sectionClass = "flex-row my-[14px] space-x-[11px]";
const voteClass = "flex-row mr-[15px] my-[7px] justify-between items-center";
const listTextClass = "flex-row shrink space-x-[11px]";
const renderMetricIndicator = metric => {
const ifAgree = ifMajorityAgree( metric );
if ( ifAgree || ifAgree === null ) {
return (
<INatIcon
testID="DQA.pass"
name="checkmark-circle"
size={19}
color={colors.inatGreen}
/>
);
}
return (
<INatIcon
name="triangle-exclamation"
size={19}
color={colors.warningRed}
/>
);
};
const renderIndicator = metric => {
const ifAgree = checkTest( metric );
if ( ifAgree || ifAgree === null ) {
return (
<INatIcon name="checkmark-circle" size={19} color={colors.inatGreen} /> );
}
return (
<INatIcon
name="triangle-exclamation"
size={19}
color={colors.warningRed}
/>
);
};
if ( isConnected === false ) {
return (
<ViewWrapper>
<OfflineNotice onPress={( ) => recheckisConnected( )} />
</ViewWrapper>
);
}
// console.log( "[DEBUG DataQualityAssessment.js] qualityMetrics?.date: ", qualityMetrics?.date );
return (
<ScrollViewWrapper testID="DataQualityAssessment">
<View className="mx-[26px] my-[19px] space-y-[9px]">
<QualityGradeStatus
qualityGrade={qualityGrade}
color={
qualityGrade === "research"
? colors.inatGreen
: colors.darkGray
}
/>
<View className="flex-row space-x-[7px]">
{isResearchGrade && (
<INatIcon
name="checkmark-circle"
size={19}
color={colors.inatGreen}
/>
)}
<Body1 className="text-darkGray">{titleOption( qualityGrade )}</Body1>
</View>
<List2 className="text-darkGray">{titleDescription( qualityGrade )}</List2>
</View>
<Divider />
<View className="mx-[15px]">
<View className={sectionClass}>
{renderIndicator( "date" )}
<Body3>{t( "Data-quality-assessment-date-specified" )}</Body3>
</View>
<Divider />
<View className={sectionClass}>
{renderIndicator( "location" )}
<Body3>{t( "Data-quality-assessment-location-specified" )}</Body3>
</View>
<Divider />
<View className={sectionClass}>
{renderIndicator( "evidence" )}
<Body3>{t( "Data-quality-assessment-has-photos-or-sounds" )}</Body3>
</View>
<Divider />
<View className={sectionClass}>
{renderIndicator( "id_supported" )}
<Body3>
{t( "Data-quality-assessment-id-supported-by-two-or-more" )}
</Body3>
</View>
<Divider />
<View className={sectionClass}>
{renderIndicator( "rank" )}
<Body3>
{t(
"Data-quality-assessment-community-taxon-species-level-or-lower"
)}
</Body3>
</View>
<Divider />
<View className={voteClass}>
<View className={listTextClass}>
{renderMetricIndicator( "date" )}
<Body3>{t( "Data-quality-assessment-date-is-accurate" )}</Body3>
</View>
<DQAVoteButtons
metric="date"
votes={qualityMetrics?.date}
setVote={setMetricVote}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
removeVote={removeMetricVote}
/>
</View>
<Divider />
<View className={voteClass}>
<View className={listTextClass}>
{renderMetricIndicator( "location" )}
<Body3>{t( "Data-quality-assessment-location-is-accurate" )}</Body3>
</View>
<DQAVoteButtons
metric="location"
votes={qualityMetrics?.location}
setVote={setMetricVote}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
removeVote={removeMetricVote}
/>
</View>
<Divider />
<View className={voteClass}>
<View className={listTextClass}>
{renderMetricIndicator( "wild" )}
<Body3>{t( "Data-quality-assessment-organism-is-wild" )}</Body3>
</View>
<DQAVoteButtons
metric="wild"
votes={qualityMetrics?.wild}
setVote={setMetricVote}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
removeVote={removeMetricVote}
/>
</View>
<Divider />
<View className={voteClass}>
<View className={listTextClass}>
{renderMetricIndicator( "evidence" )}
<Body3>{t( "Data-quality-assessment-evidence-of-organism" )}</Body3>
</View>
<DQAVoteButtons
metric="evidence"
votes={qualityMetrics?.evidence}
setVote={setMetricVote}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
removeVote={removeMetricVote}
/>
</View>
<Divider />
<View className={voteClass}>
<View className={listTextClass}>
{renderMetricIndicator( "recent" )}
<Body3>
{t( "Data-quality-assessment-recent-evidence-of-organism" )}
</Body3>
</View>
<DQAVoteButtons
metric="recent"
votes={qualityMetrics?.recent}
setVote={setMetricVote}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
removeVote={removeMetricVote}
/>
</View>
<Divider />
<View className={voteClass}>
<View className={listTextClass}>
{renderMetricIndicator( "subject" )}
<Body3>{t( "Data-quality-assessment-single-subject" )}</Body3>
</View>
<DQAVoteButtons
metric="subject"
votes={qualityMetrics?.subject}
setVote={setMetricVote}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
removeVote={removeMetricVote}
/>
</View>
<Divider />
</View>
<View className="flex-row items-center mt-5 py-2 pl-4 pr-[30px] bg-lightGray">
<Body3 className="flex-1 mr-1">
{t(
"Data-quality-assessment-can-taxon-still-be-confirmed-improved-based-on-the-evidence"
)}
</Body3>
<DQAVoteButtons
metric="needs_id"
votes={qualityMetrics?.needs_id}
setVote={setNeedsIDVote}
loadingAgree={loadingAgree}
loadingDisagree={loadingDisagree}
loadingMetric={loadingMetric}
removeVote={removeNeedsIDVote}
/>
</View>
<View className="my-[30px] mx-[15px] space-y-[11px]">
<Heading4>{t( "ABOUT-THE-DQA" )}</Heading4>
<List2>{t( "About-the-DQA-description" )}</List2>
</View>
</ScrollViewWrapper>
);
};
export default DataQualityAssessment;

View File

@@ -1,39 +0,0 @@
// @flow
import classnames from "classnames";
import {
Body2,
INatIcon
} from "components/SharedComponents";
import { View } from "components/styledComponents";
import { t } from "i18next";
import * as React from "react";
import { getShadow } from "styles/global";
import colors from "styles/tailwindColors";
const CoordinatesCopiedNotification = ( ): React.Node => (
<View
className={classnames(
"flex",
"flex-row",
"justify-center",
"items-center",
"bottom-1/2",
"bg-white",
"p-3",
"rounded-xl"
)}
style={getShadow( )}
>
<Body2 className="mr-3">
{t( "Coordinates-copied-to-clipboard" )}
</Body2>
<INatIcon
name="checkmark-circle"
size={19}
color={colors.inatGreen}
/>
</View>
);
export default CoordinatesCopiedNotification;

View File

@@ -1,182 +0,0 @@
// @flow
import createFlag from "api/flags";
import {
BackButton,
Body1, Body3, Button, Checkbox, Subheading1
} from "components/SharedComponents";
import {
Modal,
SafeAreaView,
View
} from "components/styledComponents";
import { t } from "i18next";
import type { Node } from "react";
import React, { useRef, useState } from "react";
import {
Alert,
findNodeHandle
} from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { TextInput } from "react-native-paper";
import useAuthenticatedMutation from "sharedHooks/useAuthenticatedMutation";
type Props = {
id:number,
itemType:string,
showFlagItemModal: boolean,
closeFlagItemModal: Function,
onItemFlagged: Function
}
const FlagItemModal = ( {
id, itemType, showFlagItemModal, closeFlagItemModal, onItemFlagged
}: Props ): Node => {
const keyboardScrollRef = useRef( null );
const [checkBoxValue, setCheckBoxValue] = useState( "none" );
const [explanation, setExplanation] = useState( "" );
const [loading, setLoading] = useState( false );
const showErrorAlert = error => Alert.alert(
"Error",
error,
[{ text: t( "OK" ) }],
{
cancelable: true
}
);
const scrollToInput = node => {
keyboardScrollRef?.current?.scrollToFocusedInput( node );
};
const toggleCheckBoxValue = checkbox => {
if ( checkBoxValue === checkbox ) {
setCheckBoxValue( "none" );
} else { setCheckBoxValue( checkbox ); }
};
const resetFlagModal = () => {
setCheckBoxValue( "none" );
setExplanation( "" );
closeFlagItemModal();
setLoading( false );
};
const createFlagMutation = useAuthenticatedMutation(
( params, optsWithAuth ) => createFlag( params, optsWithAuth ),
{
onSuccess: data => {
resetFlagModal();
onItemFlagged( data );
},
onError: error => {
setLoading( false );
showErrorAlert( error );
}
}
);
const submitFlag = () => {
if ( checkBoxValue !== "none" ) {
let params = {
flag: {
flaggable_type: itemType,
flaggable_id: id,
flag: checkBoxValue
}
};
if ( checkBoxValue === "other" ) {
params = { ...params, flag_explanation: explanation };
}
setLoading( true );
createFlagMutation.mutate( params );
}
};
return (
<Modal
visible={showFlagItemModal}
animationType="slide"
className="flex-1"
testID="FlagItemModal"
>
<SafeAreaView className="flex-1">
<View className="flex-row-reverse justify-between p-6 border-b">
<BackButton onPress={closeFlagItemModal} />
<Subheading1 className="text-xl">
{t( "Flag-An-Item" )}
</Subheading1>
</View>
<KeyboardAwareScrollView
ref={keyboardScrollRef}
enableOnAndroid
enableAutomaticScroll
extraHeight={200}
className="p-6"
>
<Body1 className="text-base">
{t( "Flag-Item-Description" )}
</Body1>
<View className="flex-row my-2">
<Checkbox
isChecked={checkBoxValue === "spam"}
onValueChange={() => toggleCheckBoxValue( "spam" )}
text={t( "Spam" )}
/>
</View>
<Body1 className="mb-2 text-base" style>{t( "Spam-Examples" )}</Body1>
<View className="flex-row my-2">
<Checkbox
isChecked={checkBoxValue === "inappropriate"}
onValueChange={() => toggleCheckBoxValue( "inappropriate" )}
text={t( "Offensive-Inappropriate" )}
/>
</View>
<Body1 className="mb-2 text-base">{t( "Offensive-Inappropriate-Examples" )}</Body1>
<View className="flex-row my-2">
<Checkbox
isChecked={checkBoxValue === "other"}
onValueChange={() => toggleCheckBoxValue( "other" )}
text={t( "Other" )}
/>
</View>
<Body1 className="mb-2 text-base">{t( "Flag-Item-Other-Description" )}</Body1>
{( checkBoxValue === "other" )
&& (
<>
<TextInput
className="text-sm"
placeholder={t( "Flag-Item-Other-Input-Hint" )}
value={explanation}
onChangeText={text => setExplanation( text )}
onFocus={e => scrollToInput( findNodeHandle( e.target ) )}
accessibilityLabel={t( "Reason--flag" )}
/>
<Body3>{`${explanation.length}/255`}</Body3>
</>
)}
<View className="flex-row justify-center m-4">
<Button
className="m-2"
text={t( "Cancel" )}
onPress={() => resetFlagModal()}
/>
<Button
className="m-2"
text={t( "Save" )}
onPress={submitFlag}
level="primary"
loading={loading}
/>
</View>
</KeyboardAwareScrollView>
</SafeAreaView>
</Modal>
);
};
export default FlagItemModal;

View File

@@ -1,139 +0,0 @@
// @flow
import {
ActivityIndicator,
Body3,
INatIconButton
} from "components/SharedComponents";
import { View } from "components/styledComponents";
import * as React from "react";
import { useCurrentUser, useTranslation } from "sharedHooks";
import colors from "styles/tailwindColors";
type Props = {
metric: string,
votes: [Object],
loadingAgree: boolean,
loadingDisagree: boolean,
loadingMetric: ?string,
setVote: Function,
removeVote: Function
}
const getUserVote = ( currentUser, metric, votes ) => {
if ( votes && votes.length > 0 ) {
const match = votes.find( element => ( element.user_id === currentUser?.id ) );
if ( match ) {
if ( metric === "needs_id" ) {
return match.vote_flag === true;
}
return match.agree === true;
}
}
return null;
};
const renderVoteCount = ( status, metric, votes ) => {
if ( !votes ) return null;
const count = votes
?.filter( qualityMetric => {
if ( metric === "needs_id" ) {
return qualityMetric.vote_flag === status;
}
return qualityMetric.agree === status;
} )
?.length;
if ( !count || count === 0 ) return null;
return <Body3 classname="ml-[5px]">{count}</Body3>;
};
const DQAVoteButtons = ( {
metric,
votes,
loadingAgree,
loadingDisagree,
loadingMetric,
setVote,
removeVote
}: Props ): React.Node => {
const { t } = useTranslation( );
const currentUser = useCurrentUser( );
const userAgrees = getUserVote( currentUser, metric, votes );
const activityIndicatorOffset = "mx-[7px]";
const renderAgree = () => {
if ( loadingAgree && loadingMetric === metric ) {
return ( <ActivityIndicator size={33} className={activityIndicatorOffset} /> );
}
if ( userAgrees ) {
return (
<INatIconButton
testID="DQAVoteButton.UserAgree"
icon="arrow-up-bold-circle"
size={33}
color={colors.inatGreen}
onPress={() => removeVote( { metric, vote: true } )}
accessibilityLabel={t( "Add-agreement" )}
accessibilityHint={t( "Adds-your-vote-of-agreement" )}
/>
);
}
return (
<INatIconButton
testID="DQAVoteButton.EmptyAgree"
icon="arrow-up-bold-circle-outline"
size={33}
onPress={() => setVote( { metric, vote: true } )}
accessibilityLabel={t( "Remove-agreement" )}
accessibilityHint={t( "Removes-your-vote-of-agreement" )}
/>
);
};
const renderDisagree = () => {
if ( loadingDisagree && loadingMetric === metric ) {
return ( <ActivityIndicator size={30} className={activityIndicatorOffset} /> );
}
if ( userAgrees === false ) {
return (
<INatIconButton
testID="DQAVoteButton.UserDisagree"
icon="arrow-down-bold-circle"
size={33}
color={colors.warningRed}
onPress={() => removeVote( { metric, vote: false } )}
accessibilityLabel={t( "Remove-disagreement" )}
accessibilityHint={t( "Removes-your-vote-of-disagreement" )}
/>
);
}
return (
<INatIconButton
testID="DQAVoteButton.EmptyDisagree"
icon="arrow-down-bold-circle-outline"
size={33}
onPress={() => setVote( { metric, vote: false } )}
accessibilityLabel={t( "Add-disagreement" )}
accessibilityHint={t( "Adds-your-vote-of-disagreement" )}
/>
);
};
return (
<View className="flex-row items-center justify-between w-[97px] space-x-[11px]">
<View className="flex-row items-center w-1/2">
{renderAgree()}
{renderVoteCount( true, metric, votes )}
</View>
<View className="flex-row items-center w-1/2">
{renderDisagree()}
{renderVoteCount( false, metric, votes )}
</View>
</View>
);
};
export default DQAVoteButtons;

View File

@@ -1,73 +0,0 @@
// @flow
import {
BottomSheet,
Button,
DisplayTaxon
} from "components/SharedComponents";
import { View } from "components/styledComponents";
import { t } from "i18next";
import type { Node } from "react";
import React from "react";
import { Text } from "react-native";
type Props = {
onPressClose: Function,
updateIdentification: Function,
taxon: Object
}
const showTaxon = taxon => {
if ( !taxon ) {
return <Text>{t( "Unknown-organism" )}</Text>;
}
return (
<View className="flex-row mx-[15px]">
<DisplayTaxon taxon={taxon} />
</View>
);
};
const WithdrawIDSheet = ( {
onPressClose,
updateIdentification,
taxon
}: Props ): Node => (
<BottomSheet
onPressClose={onPressClose}
headerText={t( "WITHDRAW-ID-QUESTION" )}
>
<View
className="mx-[26px] space-y-[11px] my-[15px]"
>
{showTaxon( taxon )}
</View>
<View className="flex-row justify-evenly mx-3 mb-3">
<Button
text={t( "CANCEL" )}
onPress={( ) => {
onPressClose();
}}
className="mx-2"
testID="ObsDetail.WithdrawId.cancel"
accessibilityRole="button"
accessibilityHint={t( "Closes-withdraw-id-sheet" )}
level="secondary"
/>
<Button
text={t( "WITHDRAW-ID" )}
onPress={( ) => {
updateIdentification( { current: false } );
onPressClose();
}}
className="mx-2 grow"
testID="ObsDetail.WithdrawId.withdraw"
accessibilityRole="button"
accessibilityHint={t( "Withdraws-identification" )}
level="primary"
/>
</View>
</BottomSheet>
);
export default WithdrawIDSheet;

View File

@@ -1,10 +0,0 @@
// @flow
const checkCamelAndSnakeCase = ( object: Object, camelCaseKey: string ): ?string => {
if ( !object ) { return ""; }
const snakeCaseKey = camelCaseKey.replace( /[A-Z]/g, letter => `_${letter.toLowerCase()}` );
return object[camelCaseKey] || object[snakeCaseKey];
};
export default checkCamelAndSnakeCase;

View File

@@ -1,33 +0,0 @@
import { CommonActions } from "@react-navigation/native";
// Creates a navigation tree that navigates to the ObsDetails screen with a specific obs UUID,
// and when navigating back from the ObsDetails screen, it'll go back to ObsList screen
export default function navigateToObsDetails( navigation, uuid ) {
navigation.dispatch(
CommonActions.reset( {
index: 1,
routes: [
{
name: "TabNavigator",
state: {
routes: [
{
name: "TabStackNavigator",
state: {
index: 0,
routes: [
{ name: "ObsList" },
{
name: "ObsDetails",
params: { uuid }
}
]
}
}
]
}
}
]
} )
);
}

View File

@@ -1,35 +0,0 @@
// @flow
import checkCamelAndSnakeCase from "components/ObsDetails/helpers/checkCamelAndSnakeCase";
import { Body4, INatIcon } from "components/SharedComponents";
import { View } from "components/styledComponents";
import * as React from "react";
import useTranslation from "sharedHooks/useTranslation.ts";
type Props = {
observation: Object,
};
const Geoprivacy = ( { observation }: Props ): React.Node => {
const { t } = useTranslation( );
let geoprivacy = checkCamelAndSnakeCase( observation, "geoprivacy" );
if ( !geoprivacy ) {
geoprivacy = t( "No-Location" );
}
return (
<View className="flex-row mt-[11px]">
<INatIcon name="globe-outline" size={14} />
<Body4
className="text-darkGray ml-[5px]"
numberOfLines={1}
ellipsizeMode="tail"
>
{geoprivacy}
</Body4>
</View>
);
};
export default Geoprivacy;

View File

@@ -1,29 +0,0 @@
import { INatIconButton } from "components/SharedComponents";
import React from "react";
import { useTranslation } from "sharedHooks";
type Props = {
color: string;
uniqueKey: string;
uploadSingleObservation: ( ) => void;
}
const UploadStartIcon = ( {
color,
uniqueKey,
uploadSingleObservation
}: Props ) => {
const { t } = useTranslation( );
return (
<INatIconButton
icon="upload-saved"
color={color}
size={33}
onPress={uploadSingleObservation}
disabled={false}
accessibilityLabel={t( "Start-upload" )}
testID={`UploadIcon.start.${uniqueKey}`}
/>
);
};
export default UploadStartIcon;

View File

@@ -1,27 +0,0 @@
import { useNavigation, useRoute } from "@react-navigation/native";
import {
ViewWrapper
} from "components/SharedComponents";
import { View } from "components/styledComponents";
import React, { useEffect } from "react";
import UserList from "./UserList";
const UserListContainer = ( ) => {
const navigation = useNavigation( );
const { params } = useRoute( );
const { users, headerOptions } = params;
useEffect( ( ) => {
navigation.setOptions( headerOptions );
}, [headerOptions, navigation] );
return (
<ViewWrapper>
<View className="border-b border-lightGray mt-5" />
<UserList users={users} />
</ViewWrapper>
);
};
export default UserListContainer;

View File

@@ -520,10 +520,6 @@ Filter-by-observed-on-date = Filter by observations observed on a specific date
Filter-by-uploaded-between-dates = Filter by observations uploaded between two specific dates
Filter-by-uploaded-on-date = Filter by observations uploaded on a specific date
Filters = Filters
Flag-An-Item = Flag An Item
Flag-Item-Description = Flagging brings something to the attention of volunteer site curators. Please don't flag problems you can address with identifications, the Data Quality Assessment, or by talking to the person who made the content.
Flag-Item-Other-Description = Some other reason you can explain below.
Flag-Item-Other-Input-Hint = Specify the reason you're flagging this item
# Status when an item has been flagged
Flagged = Flagged
Flash = Flash
@@ -876,8 +872,6 @@ Observers = Observers
Observers-View = Observers View
# Month of October
October = October
Offensive-Inappropriate = Offensive/Inappropriate
Offensive-Inappropriate-Examples = Misleading or illegal content, racial or ethnic slurs, etc. For more on our definition of "appropriate," see the FAQ.
Offline-DQA-description = The DQA may not be accurate. Check your internet connection and try again.
Offline-suggestions-may-differ-from-online = Offline suggestions may differ from online suggestions, and taxon images and common names may not load.
# Generic confirmation, e.g. button on a warning alert
@@ -904,10 +898,6 @@ Or-you-can-try-to-get-a-clearer-photo-by-zooming-in-getting-closer = Or, you can
# Picker prompt on observation edit
Organism-is-captive = Organism is captive
Organisms-that-are-identified-to-species = Organisms that are identified to species rank or below
# Generic option in a list for unanticipated cases, e.g. a choice to manually
# enter an explanation for why you are flagging something instead of choosing
# one of the existing options
Other = Other
OTHER-DATA = OTHER DATA
OTHER-SUGGESTIONS = OTHER SUGGESTIONS
# Tab on notifications showing notifications about content created by others.
@@ -1039,9 +1029,6 @@ Ranks-Zoosection = Zoosection
Ranks-ZOOSUBSECTION = ZOOSUBSECTION
Ranks-Zoosubsection = Zoosubsection
Read-more-on-Wikipedia = Read more on Wikipedia
# Label for the input in the form for creating a flag that allows the user to
# describe a reason they are creating the flag
Reason--flag = Reason
# Help text for the button that opens the sound recorder
Record-a-sound = Record a sound
# Title of screen asking for permission to access the microphone
@@ -1102,8 +1089,6 @@ Reviewed-observations-only = Reviewed observations only
Satellite--map-type = Satellite
# Label for a button that persists something
SAVE = SAVE
# Label for a button that persists something
Save = Save
SAVE-ALL = SAVE ALL
# Button that saves all observations in a batch of multiple observations
Save-all-observations = Save all observations
@@ -1197,8 +1182,6 @@ sound-recorder-help-Stop-moving = Stop moving
sound-recorder-help-Try-to-isolate = Try to isolate the sound of a single organism. If you cant, make sure to leave a note of which organism youre recording.
Sounds = Sounds
Source-List = <0>(Source List: </0><1>{ $source }</1><0>)</0>
Spam = Spam
Spam-Examples = Commercial solicitation, links to nowhere, etc.
Species = Species
Species-View = Species View
SPECIES-WITHOUT-NUMBER =
@@ -1540,7 +1523,6 @@ You-may-have-observed-a-species-in-this-group = You may have observed a species
You-may-have-observed-this-species = You may have observed this species
# Description for modal that pops up if user logs in and has more than 50 observations
You-may-notice-changes-to-how-things-look-and-flow = You may notice changes to how things look and flow. You can control your options in the settings.
You-must-be-logged-in-to-view-messages = You must be logged in to view messages
You-must-install-Google-Play-Services-to-sign-in-with-Google = You must install Google Play Services to sign in with Google.
# Error message when you try to do something that requires an Internet
# connection but such a connection is, tragically, missing

View File

@@ -289,10 +289,6 @@
"Filter-by-uploaded-between-dates": "Filter by observations uploaded between two specific dates",
"Filter-by-uploaded-on-date": "Filter by observations uploaded on a specific date",
"Filters": "Filters",
"Flag-An-Item": "Flag An Item",
"Flag-Item-Description": "Flagging brings something to the attention of volunteer site curators. Please don't flag problems you can address with identifications, the Data Quality Assessment, or by talking to the person who made the content.",
"Flag-Item-Other-Description": "Some other reason you can explain below.",
"Flag-Item-Other-Input-Hint": "Specify the reason you're flagging this item",
"Flagged": "Flagged",
"Flash": "Flash",
"Flip-camera": "Flip camera",
@@ -512,8 +508,6 @@
"Observers": "Observers",
"Observers-View": "Observers View",
"October": "October",
"Offensive-Inappropriate": "Offensive/Inappropriate",
"Offensive-Inappropriate-Examples": "Misleading or illegal content, racial or ethnic slurs, etc. For more on our definition of \"appropriate,\" see the FAQ.",
"Offline-DQA-description": "The DQA may not be accurate. Check your internet connection and try again.",
"Offline-suggestions-may-differ-from-online": "Offline suggestions may differ from online suggestions, and taxon images and common names may not load.",
"OK": "OK",
@@ -532,7 +526,6 @@
"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",
"Organisms-that-are-identified-to-species": "Organisms that are identified to species rank or below",
"Other": "Other",
"OTHER-DATA": "OTHER DATA",
"OTHER-SUGGESTIONS": "OTHER SUGGESTIONS",
"OTHERS--notifications": "OTHERS",
@@ -647,7 +640,6 @@
"Ranks-ZOOSUBSECTION": "ZOOSUBSECTION",
"Ranks-Zoosubsection": "Zoosubsection",
"Read-more-on-Wikipedia": "Read more on Wikipedia",
"Reason--flag": "Reason",
"Record-a-sound": "Record a sound",
"Record-animal-sounds": "Record animal sounds",
"RECORD-NEW-SOUND": "RECORD NEW SOUND",
@@ -683,7 +675,6 @@
"Reviewed-observations-only": "Reviewed observations only",
"Satellite--map-type": "Satellite",
"SAVE": "SAVE",
"Save": "Save",
"SAVE-ALL": "SAVE ALL",
"Save-all-observations": "Save all observations",
"SAVE-CHANGES": "SAVE CHANGES",
@@ -754,8 +745,6 @@
"sound-recorder-help-Try-to-isolate": "Try to isolate the sound of a single organism. If you cant, make sure to leave a note of which organism youre recording.",
"Sounds": "Sounds",
"Source-List": "<0>(Source List: </0><1>{ $source }</1><0>)</0>",
"Spam": "Spam",
"Spam-Examples": "Commercial solicitation, links to nowhere, etc.",
"Species": "Species",
"Species-View": "Species View",
"SPECIES-WITHOUT-NUMBER": "{ $count ->\n [one] SPECIES\n *[other] SPECIES\n}",
@@ -928,7 +917,6 @@
"You-may-have-observed-a-species-in-this-group": "You may have observed a species in this group",
"You-may-have-observed-this-species": "You may have observed this species",
"You-may-notice-changes-to-how-things-look-and-flow": "You may notice changes to how things look and flow. You can control your options in the settings.",
"You-must-be-logged-in-to-view-messages": "You must be logged in to view messages",
"You-must-install-Google-Play-Services-to-sign-in-with-Google": "You must install Google Play Services to sign in with Google.",
"You-need-an-Internet-connection-to-do-that": "You need an Internet connection to do that.",
"You-need-log-in-to-do-that": "You need to log in to do that.",

View File

@@ -520,10 +520,6 @@ Filter-by-observed-on-date = Filter by observations observed on a specific date
Filter-by-uploaded-between-dates = Filter by observations uploaded between two specific dates
Filter-by-uploaded-on-date = Filter by observations uploaded on a specific date
Filters = Filters
Flag-An-Item = Flag An Item
Flag-Item-Description = Flagging brings something to the attention of volunteer site curators. Please don't flag problems you can address with identifications, the Data Quality Assessment, or by talking to the person who made the content.
Flag-Item-Other-Description = Some other reason you can explain below.
Flag-Item-Other-Input-Hint = Specify the reason you're flagging this item
# Status when an item has been flagged
Flagged = Flagged
Flash = Flash
@@ -876,8 +872,6 @@ Observers = Observers
Observers-View = Observers View
# Month of October
October = October
Offensive-Inappropriate = Offensive/Inappropriate
Offensive-Inappropriate-Examples = Misleading or illegal content, racial or ethnic slurs, etc. For more on our definition of "appropriate," see the FAQ.
Offline-DQA-description = The DQA may not be accurate. Check your internet connection and try again.
Offline-suggestions-may-differ-from-online = Offline suggestions may differ from online suggestions, and taxon images and common names may not load.
# Generic confirmation, e.g. button on a warning alert
@@ -904,10 +898,6 @@ Or-you-can-try-to-get-a-clearer-photo-by-zooming-in-getting-closer = Or, you can
# Picker prompt on observation edit
Organism-is-captive = Organism is captive
Organisms-that-are-identified-to-species = Organisms that are identified to species rank or below
# Generic option in a list for unanticipated cases, e.g. a choice to manually
# enter an explanation for why you are flagging something instead of choosing
# one of the existing options
Other = Other
OTHER-DATA = OTHER DATA
OTHER-SUGGESTIONS = OTHER SUGGESTIONS
# Tab on notifications showing notifications about content created by others.
@@ -1039,9 +1029,6 @@ Ranks-Zoosection = Zoosection
Ranks-ZOOSUBSECTION = ZOOSUBSECTION
Ranks-Zoosubsection = Zoosubsection
Read-more-on-Wikipedia = Read more on Wikipedia
# Label for the input in the form for creating a flag that allows the user to
# describe a reason they are creating the flag
Reason--flag = Reason
# Help text for the button that opens the sound recorder
Record-a-sound = Record a sound
# Title of screen asking for permission to access the microphone
@@ -1102,8 +1089,6 @@ Reviewed-observations-only = Reviewed observations only
Satellite--map-type = Satellite
# Label for a button that persists something
SAVE = SAVE
# Label for a button that persists something
Save = Save
SAVE-ALL = SAVE ALL
# Button that saves all observations in a batch of multiple observations
Save-all-observations = Save all observations
@@ -1197,8 +1182,6 @@ sound-recorder-help-Stop-moving = Stop moving
sound-recorder-help-Try-to-isolate = Try to isolate the sound of a single organism. If you cant, make sure to leave a note of which organism youre recording.
Sounds = Sounds
Source-List = <0>(Source List: </0><1>{ $source }</1><0>)</0>
Spam = Spam
Spam-Examples = Commercial solicitation, links to nowhere, etc.
Species = Species
Species-View = Species View
SPECIES-WITHOUT-NUMBER =
@@ -1540,7 +1523,6 @@ You-may-have-observed-a-species-in-this-group = You may have observed a species
You-may-have-observed-this-species = You may have observed this species
# Description for modal that pops up if user logs in and has more than 50 observations
You-may-notice-changes-to-how-things-look-and-flow = You may notice changes to how things look and flow. You can control your options in the settings.
You-must-be-logged-in-to-view-messages = You must be logged in to view messages
You-must-install-Google-Play-Services-to-sign-in-with-Google = You must install Google Play Services to sign in with Google.
# Error message when you try to do something that requires an Internet
# connection but such a connection is, tragically, missing

View File

@@ -5,7 +5,6 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack";
import About from "components/About";
import Developer from "components/Developer/Developer";
import Log from "components/Developer/Log";
import NetworkLogging from "components/Developer/NetworkLogging.tsx";
import UiLibrary from "components/Developer/UiLibrary";
import UiLibraryItem from "components/Developer/UiLibraryItem";
import Donate from "components/Donate/Donate.tsx";
@@ -313,14 +312,6 @@ const TabStackNavigator = ( ): Node => {
options={{ headerTitle: debugTitle }}
/>
{ // eslint-disable-next-line no-undef
__DEV__ && (
<Stack.Screen
name="network"
component={NetworkLogging}
/>
)
}
<Stack.Screen
name="UILibrary"
component={UiLibrary}

View File

@@ -126,6 +126,7 @@ export enum WILD_STATUS {
export enum REVIEWED {
ALL = "ALL",
// eslint-disable-next-line @typescript-eslint/no-shadow
REVIEWED = "REVIEWED",
UNREVIEWED = "UNREVIEWED"
}

View File

@@ -1,91 +0,0 @@
import { NavigationContainer } from "@react-navigation/native";
import { render, screen } from "@testing-library/react-native";
import Messages from "components/Messages/Messages";
import INatPaperProvider from "providers/INatPaperProvider";
import React from "react";
import factory from "tests/factory";
import faker from "tests/helpers/faker";
const mockedNavigate = jest.fn( );
const mockMessage = factory( "RemoteMessage", {
subject: faker.lorem.sentence( )
} );
const mockUser = factory( "LocalUser" );
jest.mock( "sharedHooks/useCurrentUser", ( ) => ( {
__esModule: true,
default: ( ) => mockUser
} ) );
jest.mock( "@react-navigation/native", ( ) => {
const actualNav = jest.requireActual( "@react-navigation/native" );
return {
...actualNav,
useNavigation: ( ) => ( {
navigate: mockedNavigate
} ),
useRoute: ( ) => ( { } )
};
} );
const renderMessages = ( ) => render(
<INatPaperProvider>
<NavigationContainer>
<Messages />
</NavigationContainer>
</INatPaperProvider>
);
// We need to do some weird stuff to test results that vary based on useQuery
// return values. We can't use jest.mock to totally change the mock
// definition within a test, so instead we mock the module once and tell it
// to return an object literal attribute in response to useQuery. That would
// be a serious problem if tests get run in parallel because you can't really
// control what that value is for that specific test. To address this, we
// take advantage of the fact that Jest seems to run describe blocks
// sequentially, or at least it doesn't seem to allow tests within a describe
// block to access data from other describe blocks. Whatever's happening,
// using a single mocked return value for useQuery within a describe block
// seems to work.
const MockData = { useQueryResponse: {} };
jest.mock( "@tanstack/react-query", ( ) => ( {
useQuery: ( ) => ( MockData.useQueryResponse )
} ) );
describe( "Messages", ( ) => {
it( "should not have accessibility errors", () => {
const messages = <Messages />;
expect( messages ).toBeAccessible();
} );
describe( "when loading", ( ) => {
beforeAll( ( ) => {
MockData.useQueryResponse = {
data: [],
isLoading: true,
isError: false
};
} );
it( "displays activity indicator when loading", ( ) => {
renderMessages( );
expect( screen.getByTestId( "Messages.activityIndicator" ) ).toBeTruthy( );
} );
} );
describe( "when loading complete", ( ) => {
beforeAll( ( ) => {
MockData.useQueryResponse = {
data: [mockMessage],
isLoading: false,
isError: false
};
} );
it( "displays message subject and not activity indicator when loading complete", ( ) => {
renderMessages( );
expect( screen.getByText( mockMessage.subject ) ).toBeTruthy( );
expect( screen.queryByTestId( "Messages.activityIndicator" ) ).toBeNull( );
} );
} );
} );

View File

@@ -1,118 +0,0 @@
import { screen } from "@testing-library/react-native";
import FlagItemModal from "components/ObsDetails/FlagItemModal";
import React from "react";
import factory from "tests/factory";
import { renderComponent } from "tests/helpers/render";
const mockCallback = jest.fn();
const mockObservation = factory( "LocalObservation", {
created_at: "2022-11-27T19:07:41-08:00",
time_observed_at: "2023-12-14T21:07:41-09:30"
} );
jest.mock( "sharedHelpers/dateAndTime", ( ) => ( {
__esModule: true,
formatIdDate: jest.fn()
} ) );
jest.mock( "providers/contexts", ( ) => ( {
__esModule: true,
RealmContext: {
useRealm: jest.fn()
}
} ) );
// TODO if/when we test mutation behavior, the mutation will need to be mocked
// so it actually does something, or we need to take a different approach
const mockMutate = jest.fn();
jest.mock( "sharedHooks/useAuthenticatedMutation", ( ) => ( {
__esModule: true,
default: ( ) => ( {
mutate: mockMutate
} )
} ) );
jest.mock( "../../../../src/components/LoginSignUp/AuthenticationService", ( ) => ( {
getUserId: ( ) => mockObservation.user.id,
isCurrentUser: ( ) => false
} ) );
jest.mock( "react-native-keyboard-aware-scroll-view", () => {
const KeyboardAwareScrollView = require( "react-native" ).ScrollView;
return { KeyboardAwareScrollView };
} );
describe( "Flags", ( ) => {
// it( "renders activity item with Flag Button", async ( ) => {
// renderComponent(
// <PaperProvider>
// <ActivityItem item={mockIdentification} />
// </PaperProvider>
// );
// expect( await screen.findByTestId( "KebabMenu.Button" ) ).toBeTruthy( );
// expect( await screen.findByTestId( "FlagItemModal" ) ).toBeTruthy();
// expect( await screen.findByTestId( "FlagItemModal" ) )
// .toHaveProperty( "props.visible", false );
// fireEvent.press( await screen.findByTestId( "KebabMenu.Button" ) );
// expect( await screen.findByTestId( "MenuItem.Flag" ) ).toBeTruthy( );
// expect( screen.getByText( "Flag" ) ).toBeTruthy( );
// } );
// it( "renders Flag Modal when Flag button pressed", async ( ) => {
// renderComponent(
// <PaperProvider>
// <ActivityItem item={mockIdentification} />
// </PaperProvider>
// );
// expect( await screen.findByTestId( "KebabMenu.Button" ) ).toBeTruthy( );
// expect( await screen.findByTestId( "FlagItemModal" ) ).toBeTruthy();
// expect( await screen.findByTestId( "FlagItemModal" ) )
// .toHaveProperty( "props.visible", false );
// fireEvent.press( await screen.findByTestId( "KebabMenu.Button" ) );
// expect( await screen.findByTestId( "MenuItem.Flag" ) ).toBeTruthy( );
// fireEvent.press( await screen.findByTestId( "MenuItem.Flag" ) );
// expect( await screen.findByTestId( "FlagItemModal" ) )
// .toHaveProperty( "props.visible", true );
// expect( screen.getByText( "Flag An Item" ) ).toBeTruthy( );
// } );
it( "renders Flag Modal content", async ( ) => {
renderComponent(
<FlagItemModal
id="000"
itemType="foo"
showFlagItemModal
closeFlagItemModal={mockCallback}
onItemFlagged={mockCallback}
/>
);
expect( await screen.findByText( "Flag An Item" ) ).toBeTruthy( );
expect( screen.getByText( "Spam" ) ).toBeTruthy( );
expect( screen.getByText( "Offensive/Inappropriate" ) ).toBeTruthy( );
expect( screen.getByText( "Other" ) ).toBeTruthy( );
expect( screen.getAllByRole( "radio" ) ).toHaveLength( 3 );
} );
// it( "calls flag api when save button pressed", async ( ) => {
// renderComponent(
// <FlagItemModal
// id="000"
// itemType="foo"
// showFlagItemModal
// closeFlagItemModal={mockCallback}
// onItemFlagged={mockCallback}
// />
// );
// expect( await screen.findByText( "Flag An Item" ) ).toBeTruthy( );
// expect( screen.getByText( "Spam" ) ).toBeTruthy( );
// expect( screen.queryAllByRole( "checkbox" ) ).toHaveLength( 3 );
// fireEvent.press( screen.queryByText( "Spam" ) );
// expect( await screen.findByText( "Save" ) ).toBeTruthy( );
// fireEvent.press( screen.queryByText( "Save" ) );
// expect( await mockMutate ).toHaveBeenCalled();
// } );
} );

View File

@@ -1,66 +0,0 @@
import { fireEvent, screen } from "@testing-library/react-native";
import HeaderKebabMenu from "components/ObsDetails/HeaderKebabMenu";
import i18next from "i18next";
import React from "react";
import { Platform, Share } from "react-native";
import { renderComponent } from "tests/helpers/render";
jest.mock( "react-native/Libraries/Share/Share", () => ( {
share: jest.fn( () => Promise.resolve( "mockResolve" ) )
} ) );
jest.mock( "react-native/Libraries/Utilities/Platform", ( ) => ( {
OS: "ios",
select: jest.fn( )
} ) );
const observationId = 1234;
const url = `https://www.inaturalist.org/observations/${observationId}`;
describe( "HeaderKebabMenu", () => {
it( "renders and opens kebab menu share button", async ( ) => {
renderComponent( <HeaderKebabMenu observationId={observationId} /> );
const anchorButton = screen.getByLabelText( i18next.t( "Observation-options" ) );
expect( anchorButton ).toBeTruthy( );
fireEvent.press( anchorButton );
const shareButton = await screen.findByTestId( "MenuItem.Share" );
expect( shareButton ).toBeTruthy( );
} );
it( "opens native share dialog with expected url", async ( ) => {
renderComponent( <HeaderKebabMenu observationId={observationId} /> );
const anchorButton = screen.getByLabelText( i18next.t( "Observation-options" ) );
expect( anchorButton ).toBeTruthy( );
fireEvent.press( anchorButton );
const shareButton = await screen.findByTestId( "MenuItem.Share" );
expect( shareButton ).toBeTruthy( );
fireEvent.press( shareButton );
expect( Share.share ).toHaveBeenCalledTimes( 1 );
expect( Share.share ).toHaveBeenCalledWith( { message: "", url } );
} );
const defaultPlatformOS = Platform.OS;
describe( "on Android", ( ) => {
// TODO change this to a less brittle and sweeping approach. Some ideas at
// https://stackoverflow.com/questions/43161416/mocking-platform-detection-in-jest-and-react-native
beforeAll( ( ) => {
Platform.OS = "android";
} );
afterAll( ( ) => {
Platform.OS = defaultPlatformOS;
} );
it( "opens native share dialog with expected message", async ( ) => {
renderComponent( <HeaderKebabMenu observationId={observationId} /> );
const anchorButton = screen.getByLabelText( i18next.t( "Observation-options" ) );
expect( anchorButton ).toBeTruthy( );
fireEvent.press( anchorButton );
const shareButton = await screen.findByTestId( "MenuItem.Share" );
expect( shareButton ).toBeTruthy( );
fireEvent.press( shareButton );
expect( Share.share ).toHaveBeenCalledWith( { message: url, url: "" } );
} );
} );
} );

View File

@@ -1,103 +0,0 @@
import { screen } from "@testing-library/react-native";
import DQAVoteButtons from "components/ObsDetailsDefaultMode/MoreSection/DQAVoteButtons";
import React from "react";
import factory from "tests/factory";
import { renderComponent } from "tests/helpers/render";
const mockUser = factory( "RemoteUser" );
const mockQualityMetrics = [
{
id: 0,
agree: true,
metric: "wild",
user_id: mockUser.id
}
];
const mockVotesNeedsID = [
factory( "RemoteVote", {
vote_scope: "needs_id",
user: mockUser,
user_id: mockUser.id
} )
];
// Mock useCurrentUser hook
jest.mock( "sharedHooks/useCurrentUser", ( ) => ( {
__esModule: true,
default: jest.fn( ( ) => ( {
id: mockUser.id
} ) )
} ) );
describe( "DQA Vote Buttons for wild metric", ( ) => {
test( "renders correct DQA user vote", async ( ) => {
renderComponent( <DQAVoteButtons
metric="wild"
votes={mockQualityMetrics}
setVote={jest.fn()}
loadingAgree={jest.fn()}
loadingDisagree={jest.fn()}
loadingMetric={jest.fn()}
removeVote={jest.fn()}
/> );
const button = await screen.findByTestId(
"DQAVoteButton.UserAgree"
);
expect( button ).toBeTruthy( );
} );
test( "renders correct DQA user vote number", async ( ) => {
renderComponent( <DQAVoteButtons
metric="wild"
votes={mockQualityMetrics}
setVote={jest.fn()}
loadingAgree={jest.fn()}
loadingDisagree={jest.fn()}
loadingMetric={jest.fn()}
removeVote={jest.fn()}
/> );
const voteNumber = await screen.findByText(
"1"
);
expect( voteNumber ).toBeTruthy( );
} );
} );
describe( "DQA Vote Buttons for needs_id metric", ( ) => {
test( "renders correct DQA user vote", async ( ) => {
renderComponent( <DQAVoteButtons
metric="needs_id"
votes={mockVotesNeedsID}
setVote={jest.fn()}
loadingAgree={jest.fn()}
loadingDisagree={jest.fn()}
loadingMetric={jest.fn()}
removeVote={jest.fn()}
/> );
const button = await screen.findByTestId(
"DQAVoteButton.UserAgree"
);
expect( button ).toBeTruthy( );
} );
test( "renders correct DQA user vote number", async ( ) => {
renderComponent( <DQAVoteButtons
metric="needs_id"
votes={mockVotesNeedsID}
setVote={jest.fn()}
loadingAgree={jest.fn()}
loadingDisagree={jest.fn()}
loadingMetric={jest.fn()}
removeVote={jest.fn()}
/> );
const voteNumber = await screen.findByText(
"1"
);
expect( voteNumber ).toBeTruthy( );
} );
} );

View File

@@ -1,118 +0,0 @@
import { screen } from "@testing-library/react-native";
import FlagItemModal from "components/ObsDetailsDefaultMode/FlagItemModal";
import React from "react";
import factory from "tests/factory";
import { renderComponent } from "tests/helpers/render";
const mockCallback = jest.fn();
const mockObservation = factory( "LocalObservation", {
created_at: "2022-11-27T19:07:41-08:00",
time_observed_at: "2023-12-14T21:07:41-09:30"
} );
jest.mock( "sharedHelpers/dateAndTime", ( ) => ( {
__esModule: true,
formatIdDate: jest.fn()
} ) );
jest.mock( "providers/contexts", ( ) => ( {
__esModule: true,
RealmContext: {
useRealm: jest.fn()
}
} ) );
// TODO if/when we test mutation behavior, the mutation will need to be mocked
// so it actually does something, or we need to take a different approach
const mockMutate = jest.fn();
jest.mock( "sharedHooks/useAuthenticatedMutation", ( ) => ( {
__esModule: true,
default: ( ) => ( {
mutate: mockMutate
} )
} ) );
jest.mock( "../../../../src/components/LoginSignUp/AuthenticationService", ( ) => ( {
getUserId: ( ) => mockObservation.user.id,
isCurrentUser: ( ) => false
} ) );
jest.mock( "react-native-keyboard-aware-scroll-view", () => {
const KeyboardAwareScrollView = require( "react-native" ).ScrollView;
return { KeyboardAwareScrollView };
} );
describe( "Flags", ( ) => {
// it( "renders activity item with Flag Button", async ( ) => {
// renderComponent(
// <PaperProvider>
// <ActivityItem item={mockIdentification} />
// </PaperProvider>
// );
// expect( await screen.findByTestId( "KebabMenu.Button" ) ).toBeTruthy( );
// expect( await screen.findByTestId( "FlagItemModal" ) ).toBeTruthy();
// expect( await screen.findByTestId( "FlagItemModal" ) )
// .toHaveProperty( "props.visible", false );
// fireEvent.press( await screen.findByTestId( "KebabMenu.Button" ) );
// expect( await screen.findByTestId( "MenuItem.Flag" ) ).toBeTruthy( );
// expect( screen.getByText( "Flag" ) ).toBeTruthy( );
// } );
// it( "renders Flag Modal when Flag button pressed", async ( ) => {
// renderComponent(
// <PaperProvider>
// <ActivityItem item={mockIdentification} />
// </PaperProvider>
// );
// expect( await screen.findByTestId( "KebabMenu.Button" ) ).toBeTruthy( );
// expect( await screen.findByTestId( "FlagItemModal" ) ).toBeTruthy();
// expect( await screen.findByTestId( "FlagItemModal" ) )
// .toHaveProperty( "props.visible", false );
// fireEvent.press( await screen.findByTestId( "KebabMenu.Button" ) );
// expect( await screen.findByTestId( "MenuItem.Flag" ) ).toBeTruthy( );
// fireEvent.press( await screen.findByTestId( "MenuItem.Flag" ) );
// expect( await screen.findByTestId( "FlagItemModal" ) )
// .toHaveProperty( "props.visible", true );
// expect( screen.getByText( "Flag An Item" ) ).toBeTruthy( );
// } );
it( "renders Flag Modal content", async ( ) => {
renderComponent(
<FlagItemModal
id="000"
itemType="foo"
showFlagItemModal
closeFlagItemModal={mockCallback}
onItemFlagged={mockCallback}
/>
);
expect( await screen.findByText( "Flag An Item" ) ).toBeTruthy( );
expect( screen.getByText( "Spam" ) ).toBeTruthy( );
expect( screen.getByText( "Offensive/Inappropriate" ) ).toBeTruthy( );
expect( screen.getByText( "Other" ) ).toBeTruthy( );
expect( screen.getAllByRole( "radio" ) ).toHaveLength( 3 );
} );
// it( "calls flag api when save button pressed", async ( ) => {
// renderComponent(
// <FlagItemModal
// id="000"
// itemType="foo"
// showFlagItemModal
// closeFlagItemModal={mockCallback}
// onItemFlagged={mockCallback}
// />
// );
// expect( await screen.findByText( "Flag An Item" ) ).toBeTruthy( );
// expect( screen.getByText( "Spam" ) ).toBeTruthy( );
// expect( screen.queryAllByRole( "checkbox" ) ).toHaveLength( 3 );
// fireEvent.press( screen.queryByText( "Spam" ) );
// expect( await screen.findByText( "Save" ) ).toBeTruthy( );
// fireEvent.press( screen.queryByText( "Save" ) );
// expect( await mockMutate ).toHaveBeenCalled();
// } );
} );

View File

@@ -1,37 +0,0 @@
import { screen } from "@testing-library/react-native";
import WithdrawIDSheet from "components/ObsDetailsDefaultMode/Sheets/WithdrawIDSheet";
import { t } from "i18next";
import React from "react";
import factory from "tests/factory";
import { renderComponent } from "tests/helpers/render";
const mockTaxon = factory( "RemoteTaxon", {
name: "Plantae",
iconic_taxon_name: "Plantae"
} );
const mockMutate = jest.fn();
const mockHandleClose = jest.fn();
describe( "WithdrawIDSheet", () => {
it( "renders sheet correctly", async ( ) => {
renderComponent(
<WithdrawIDSheet
onPressClose={mockHandleClose}
withdrawOrRestoreIdentification={mockMutate}
taxon={mockTaxon}
/>
);
expect( await screen.findByText( t( "WITHDRAW-ID-QUESTION" ) ) ).toBeTruthy( );
expect( screen.getByRole(
"button",
{ name: t( "WITHDRAW-ID" ), disabled: false }
) ).toBeTruthy( );
expect( screen.getByRole(
"button",
{ name: t( "CANCEL" ), disabled: false }
) ).toBeTruthy( );
} );
} );