MOB-925 - new designs for add-obs button sheet

This commit is contained in:
Yaron Budowski
2025-10-24 01:33:35 +01:00
committed by Abbey Campbell
parent a564f85ba0
commit 9c2facc680
16 changed files with 233 additions and 488 deletions

View File

@@ -0,0 +1,175 @@
import classNames from "classnames";
import {
Body3, BottomSheet, INatIconButton
} from "components/SharedComponents";
import { View } from "components/styledComponents";
import React, { useMemo } from "react";
import { Platform, TouchableOpacity } from "react-native";
import Observation from "realmModels/Observation";
import { useTranslation } from "sharedHooks";
import useStore from "stores/useStore";
import { getShadow } from "styles/global";
import colors from "styles/tailwindColors";
interface Props {
closeModal: ( ) => void;
navAndCloseModal: ( screen: string, params?: {
camera?: string
} ) => void;
hidden: boolean;
}
export type ObsCreateItem = {
text?: string,
icon: string,
onPress: ( ) => void,
testID: string,
className: string,
accessibilityLabel: string,
accessibilityHint: string
}
const majorVersionIOS = parseInt( String( Platform.Version ), 10 );
const AI_CAMERA_SUPPORTED = ( Platform.OS === "ios" && majorVersionIOS >= 11 )
|| ( Platform.OS === "android" && Platform.Version > 21 );
const DROP_SHADOW = getShadow( {
offsetHeight: 1,
elevation: 1,
shadowRadius: 1
} );
const GREEN_CIRCLE_CLASS = "bg-inatGreen rounded-full h-[36px] w-[36px] mb-2";
const ROW_CLASS = "flex-row justify-center space-x-4 w-full flex-1";
const AddObsBottomSheet = ( {
closeModal, navAndCloseModal, hidden
}: Props ) => {
const { t } = useTranslation( );
const prepareObsEdit = useStore( state => state.prepareObsEdit );
const obsCreateItems = useMemo( ( ) => ( {
aiCamera: {
text: t( "ID-with-AI-Camera" ),
icon: "aicamera",
onPress: ( ) => navAndCloseModal( "Camera", { camera: "AI" } ),
testID: "aicamera-button",
accessibilityLabel: t( "AI-Camera" ),
accessibilityHint: t( "Navigates-to-AI-camera" )
},
standardCamera: {
text: t( "Take-photos" ),
icon: "camera",
onPress: ( ) => navAndCloseModal( "Camera", { camera: "Standard" } ),
testID: "camera-button",
accessibilityLabel: t( "Camera" ),
accessibilityHint: t( "Navigates-to-camera" )
},
photoLibrary: {
text: t( "Upload-photos" ),
icon: "photo-library",
onPress: ( ) => navAndCloseModal( "PhotoLibrary" ),
testID: "import-media-button",
accessibilityLabel: t( "Photo-importer" ),
accessibilityHint: t( "Navigates-to-photo-importer" )
},
soundRecorder: {
text: t( "Record-a-sound" ),
icon: "microphone",
onPress: ( ) => navAndCloseModal( "SoundRecorder" ),
testID: "record-sound-button",
accessibilityLabel: t( "Sound-recorder" ),
accessibilityHint: t( "Navigates-to-sound-recorder" )
},
noEvidence: {
text: t( "Create-observation-with-no-evidence" ),
icon: "noevidence",
onPress: async ( ) => {
const newObservation = await Observation.new( );
prepareObsEdit( newObservation );
navAndCloseModal( "ObsEdit" );
},
testID: "observe-without-evidence-button",
accessibilityLabel: t( "Observation-with-no-evidence" ),
accessibilityHint: t( "Navigates-to-observation-edit-screen" )
}
} ), [
navAndCloseModal,
prepareObsEdit,
t
] );
const renderAddObsIcon = ( {
accessibilityHint,
accessibilityLabel,
icon,
onPress,
testID,
text
}: ObsCreateItem ) => (
<TouchableOpacity
className={classNames(
"bg-white w-1/2 flex-column items-center py-4 rounded-sm flex-1",
DROP_SHADOW
)}
onPress={onPress}
accessibilityHint={accessibilityHint}
accessibilityLabel={accessibilityLabel}
testID={testID}
>
<INatIconButton
className={GREEN_CIRCLE_CLASS}
accessibilityHint={accessibilityHint}
accessibilityLabel={accessibilityLabel}
color={String( colors?.white )}
icon={icon}
size={icon === "aicamera"
? 28
: 20}
/>
<Body3>{text}</Body3>
</TouchableOpacity>
);
return (
<BottomSheet
onPressClose={closeModal}
hidden={hidden}
insideModal={false}
hideCloseButton
additionalClasses="bg-lightGray pt-4"
>
<View className="flex-column gap-y-4 pb-4 px-4">
<View className={ROW_CLASS}>
{renderAddObsIcon( obsCreateItems.standardCamera )}
{renderAddObsIcon( obsCreateItems.photoLibrary )}
</View>
<View className={ROW_CLASS}>
{renderAddObsIcon( obsCreateItems.soundRecorder )}
{AI_CAMERA_SUPPORTED && renderAddObsIcon( obsCreateItems.aiCamera )}
</View>
<View className={ROW_CLASS}>
<TouchableOpacity
className="bg-mediumGray w-full flex-row items-center py-2 px-4 rounded-sm"
onPress={obsCreateItems.noEvidence.onPress}
accessibilityHint={obsCreateItems.noEvidence.accessibilityHint}
accessibilityLabel={obsCreateItems.noEvidence.accessibilityLabel}
testID={obsCreateItems.noEvidence.testID}
>
<INatIconButton
className="h-[36px] w-[36px] mr-2"
accessibilityHint={obsCreateItems.noEvidence.accessibilityHint}
accessibilityLabel={obsCreateItems.noEvidence.accessibilityLabel}
color={String( colors?.darkGray )}
icon={obsCreateItems.noEvidence.icon}
size={24}
/>
<Body3>{obsCreateItems.noEvidence.text}</Body3>
</TouchableOpacity>
</View>
</View>
</BottomSheet>
);
};
export default AddObsBottomSheet;

View File

@@ -1,8 +1,7 @@
// @flow
import { CommonActions, useNavigation } from "@react-navigation/native";
import AddObsModal from "components/AddObsModal/AddObsModal";
import { Modal } from "components/SharedComponents";
import AddObsBottomSheet from "components/AddObsBottomSheet/AddObsBottomSheet";
import GradientButton from "components/SharedComponents/Buttons/GradientButton";
import { t } from "i18next";
import { getCurrentRoute } from "navigation/navigationUtils";
@@ -26,7 +25,6 @@ const AddObsButton = ( ): React.Node => {
// Controls whether to show the tooltip, and to show it only once to the user
const showKey = "AddObsButtonTooltip";
const shownOnce = useStore( state => state.layout.shownOnce );
const setShownOnce = useStore( state => state.layout.setShownOnce );
const justFinishedSignup = useStore( state => state.layout.justFinishedSignup );
const numOfUserObservations = zustandStorage.getItem( "numOfUserObservations" );
// Base trigger condition in all cases:
@@ -122,30 +120,13 @@ const AddObsButton = ( ): React.Node => {
};
const navToARCamera = ( ) => { navAndCloseModal( "Camera", { camera: "AI" } ); };
const addObsModal = (
<AddObsModal
closeModal={closeModal}
navAndCloseModal={navAndCloseModal}
tooltipIsVisible={tooltipIsVisible}
dismissTooltip={( ) => {
if ( tooltipIsVisible ) setShownOnce( showKey );
}}
/>
);
return (
<>
{/* match the animation timing on FadeInView.tsx */}
<Modal
animationIn="fadeIn"
animationOut="fadeOut"
animationInTiming={250}
animationOutTiming={250}
showModal={showModal}
closeModal={tooltipIsVisible
? undefined
: closeModal}
modal={addObsModal}
<AddObsBottomSheet
closeModal={closeModal}
hidden={!showModal}
navAndCloseModal={navAndCloseModal}
/>
<GradientButton
sizeClassName="w-[69px] h-[69px] mb-[5px]"

View File

@@ -1,192 +0,0 @@
import classnames from "classnames";
import {
Body2,
INatIconButton
} from "components/SharedComponents";
import GradientButton from "components/SharedComponents/Buttons/GradientButton";
import { View } from "components/styledComponents";
import React, { useMemo } from "react";
import { Platform, StatusBar } from "react-native";
import Observation from "realmModels/Observation";
import { useTranslation } from "sharedHooks";
import useStore from "stores/useStore";
import colors from "styles/tailwindColors";
import AddObsModalHelp, { ObsCreateItem } from "./AddObsModalHelp";
interface Props {
closeModal: ( ) => void;
navAndCloseModal: ( screen: string, params?: {
camera?: string
} ) => void;
tooltipIsVisible: boolean;
dismissTooltip: () => void;
}
const majorVersionIOS = parseInt( String( Platform.Version ), 10 );
const AI_CAMERA_SUPPORTED = ( Platform.OS === "ios" && majorVersionIOS >= 11 )
|| ( Platform.OS === "android" && Platform.Version > 21 );
const GREEN_CIRCLE_CLASS = "bg-inatGreen rounded-full h-[46px] w-[46px]";
const ROW_CLASS = "flex-row justify-center";
const MARGINS = AI_CAMERA_SUPPORTED
? {
standardCamera: "mr-[37px] bottom-[1px]",
photoLibrary: "ml-[37px] bottom-[1px]",
noEvidence: "mr-[26px]",
soundRecorder: "ml-[26px]"
}
: {
standardCamera: "mr-[9px]",
photoLibrary: "ml-[9px]",
noEvidence: "mr-[20px] bottom-[33px]",
soundRecorder: "ml-[20px] bottom-[33px]"
};
const AddObsModal = ( {
closeModal, navAndCloseModal, tooltipIsVisible, dismissTooltip
}: Props ) => {
const { t } = useTranslation( );
const prepareObsEdit = useStore( state => state.prepareObsEdit );
const obsCreateItems = useMemo( ( ) => ( {
aiCamera: {
text: t( "Use-iNaturalists-AI-Camera" ),
icon: "aicamera",
onPress: ( ) => navAndCloseModal( "Camera", { camera: "AI" } ),
testID: "aicamera-button",
className: classnames( GREEN_CIRCLE_CLASS, "absolute bottom-[26px]" ),
accessibilityLabel: t( "AI-Camera" ),
accessibilityHint: t( "Navigates-to-AI-camera" )
},
standardCamera: {
text: t( "Take-multiple-photos-of-a-single-organism" ),
icon: "camera",
onPress: ( ) => navAndCloseModal( "Camera", { camera: "Standard" } ),
testID: "camera-button",
accessibilityLabel: t( "Camera" ),
accessibilityHint: t( "Navigates-to-camera" ),
className: classnames( GREEN_CIRCLE_CLASS, MARGINS.standardCamera )
},
photoLibrary: {
text: t( "Upload-photos-from-your-photo-library" ),
icon: "photo-library",
onPress: ( ) => navAndCloseModal( "PhotoLibrary" ),
testID: "import-media-button",
className: classnames( GREEN_CIRCLE_CLASS, MARGINS.photoLibrary ),
accessibilityLabel: t( "Photo-importer" ),
accessibilityHint: t( "Navigates-to-photo-importer" )
},
soundRecorder: {
text: t( "Record-a-sound" ),
icon: "microphone",
onPress: ( ) => navAndCloseModal( "SoundRecorder" ),
testID: "record-sound-button",
className: classnames( GREEN_CIRCLE_CLASS, MARGINS.soundRecorder ),
accessibilityLabel: t( "Sound-recorder" ),
accessibilityHint: t( "Navigates-to-sound-recorder" )
},
noEvidence: {
text: t( "Create-an-observation-evidence" ),
icon: "noevidence",
onPress: async ( ) => {
const newObservation = await Observation.new( );
prepareObsEdit( newObservation );
navAndCloseModal( "ObsEdit" );
},
testID: "observe-without-evidence-button",
className: classnames( GREEN_CIRCLE_CLASS, MARGINS.noEvidence ),
accessibilityLabel: t( "Observation-with-no-evidence" ),
accessibilityHint: t( "Navigates-to-observation-edit-screen" )
},
closeButton: {
testID: "close-camera-options-button",
icon: "close",
className: classnames( GREEN_CIRCLE_CLASS, "h-[69px] w-[69px]" ),
onPress: closeModal,
accessibilityLabel: t( "Close" ),
accessibilityHint: t( "Closes-new-observation-options" )
}
} ), [
closeModal,
navAndCloseModal,
prepareObsEdit,
t
] );
const renderAddObsIcon = ( {
accessibilityHint,
accessibilityLabel,
className,
icon,
onPress,
testID
}: ObsCreateItem ) => (
<INatIconButton
accessibilityHint={accessibilityHint}
accessibilityLabel={accessibilityLabel}
className={className}
color={String( colors?.white )}
icon={icon}
onPress={onPress}
size={icon === "aicamera"
? 38
: 30}
testID={testID}
/>
);
const renderContent = ( ) => {
if ( tooltipIsVisible ) {
return (
<View className="justify-center items-center">
<View className="bg-white rounded-2xl px-5 py-4">
<Body2>{t( "Press-and-hold-to-view-more-options" )}</Body2>
</View>
<View
className={classnames(
// I could not figure out how to use "border-x-transparent",
"border-l-[10px] border-r-[10px] border-x-[#00000000]",
"border-t-[16px] border-t-white mb-2"
)}
/>
<GradientButton
sizeClassName="w-[69px] h-[69px]"
onPress={() => {}}
onLongPress={() => dismissTooltip( )}
accessibilityLabel={t( "Add-observations" )}
accessibilityHint={t( "Shows-observation-creation-options" )}
/>
</View>
);
}
return (
<>
<AddObsModalHelp obsCreateItems={obsCreateItems} />
<View className={classnames( ROW_CLASS, {
"bottom-[20px]": !AI_CAMERA_SUPPORTED
} )}
>
{renderAddObsIcon( obsCreateItems.standardCamera )}
{AI_CAMERA_SUPPORTED && renderAddObsIcon( obsCreateItems.aiCamera )}
{renderAddObsIcon( obsCreateItems.photoLibrary )}
</View>
<View className={classnames( ROW_CLASS, "items-center" )}>
{renderAddObsIcon( obsCreateItems.noEvidence )}
{renderAddObsIcon( obsCreateItems.closeButton )}
{renderAddObsIcon( obsCreateItems.soundRecorder )}
</View>
</>
);
};
return (
<>
<StatusBar barStyle="light-content" backgroundColor="black" />
{ renderContent( ) }
</>
);
};
export default AddObsModal;

View File

@@ -1,116 +0,0 @@
import classnames from "classnames";
import {
Body3,
Heading2,
INatIcon,
INatIconButton
} from "components/SharedComponents";
import { Pressable, View } from "components/styledComponents";
import React, { useState } from "react";
import { useDeviceOrientation, useTranslation } from "sharedHooks";
import { storage } from "stores/useStore";
import colors from "styles/tailwindColors";
export type ObsCreateItem = {
text?: string,
icon: string,
onPress: ( ) => void,
testID: string,
className: string,
accessibilityLabel: string,
accessibilityHint: string
}
type Props = {
obsCreateItems: {
[addType: string]: ObsCreateItem
}
};
const HIDE_ADD_OBS_HELP_TEXT = "hideAddObsHelpText";
const AddObsModalHelp = ( {
obsCreateItems
}: Props ) => {
const { t } = useTranslation( );
const { screenHeight } = useDeviceOrientation( );
const [hideHelpText, setHideHelpText] = useState( storage.getBoolean( HIDE_ADD_OBS_HELP_TEXT ) );
// targeting iPhone SE, which has height of 667
const isSmallScreen = screenHeight < 670;
if ( hideHelpText ) return null;
return (
<View
className={classnames( "bg-white rounded-3xl py-[23px] mb-20", {
"py-[5px] mb-10": isSmallScreen
} )}
>
<View className={classnames( "flex-row items-center mb-2" )}>
<Heading2
maxFontSizeMultiplier={1.5}
testID="identify-text"
className={classnames( "pl-[25px]", {
"px-8 -mb-2 mt-2": isSmallScreen
} )}
>
{t( "Identify-an-organism" )}
</Heading2>
<View className={classnames( "ml-auto pr-[12px]", {
"pb-6": isSmallScreen
} )}
>
<INatIconButton
icon="close"
color={String( colors?.darkGray )}
size={19}
onPress={async ( ) => {
setHideHelpText( true );
storage.set( HIDE_ADD_OBS_HELP_TEXT, true );
}}
accessibilityLabel={t( "Close" )}
accessibilityHint={t( "Closes-new-observation-explanation" )}
/>
</View>
</View>
<View className={classnames( "px-[23px]", {
"px-[10px]": isSmallScreen
} )}
>
{Object.keys( obsCreateItems )
.filter( k => k !== "closeButton" )
.map( k => {
const item = obsCreateItems[k];
return (
<Pressable
accessibilityRole="button"
className={classnames( "flex-row items-center p-2 my-1", {
"p-1": isSmallScreen
} )}
key={k}
onPress={item.onPress}
>
<INatIcon
name={item.icon}
size={item.icon === "aicamera"
? 30
: 26}
color={String(
item.icon === "aicamera"
? colors?.inatGreen
: colors?.darkGray
)}
/>
<Body3 maxFontSizeMultiplier={1.5} className="ml-[20px] shrink">
{item.text}
</Body3>
</Pressable>
);
} )}
</View>
</View>
);
};
export default AddObsModalHelp;

View File

@@ -1,4 +1,4 @@
import AddObsButton from "components/AddObsModal/AddObsButton";
import AddObsButton from "components/AddObsBottomSheet/AddObsButton";
import {
Body1,
Body2,

View File

@@ -1,6 +1,6 @@
// @flow
import { useNavigation } from "@react-navigation/native";
import AddObsModal from "components/AddObsModal/AddObsModal";
import AddObsBottomSheet from "components/AddObsBottomSheet/AddObsBottomSheet";
import { AccountCreationCard } from "components/OnboardingModal/PivotCards";
import {
HeaderUser,
@@ -8,7 +8,6 @@ import {
ViewWrapper
} from "components/SharedComponents";
import GradientButton from "components/SharedComponents/Buttons/GradientButton";
import Modal from "components/SharedComponents/Modal";
import {
Pressable, View
} from "components/styledComponents";
@@ -77,15 +76,10 @@ const MyObservationsEmptySimple = ( { currentUser, isConnected, justFinishedSign
/>
</View>
</View>
<Modal
showModal={showModal}
<AddObsBottomSheet
closeModal={( ) => setShowModal( false )}
modal={(
<AddObsModal
closeModal={( ) => setShowModal( false )}
navAndCloseModal={navAndCloseModal}
/>
)}
hidden={!showModal}
navAndCloseModal={navAndCloseModal}
/>
</ViewWrapper>

View File

@@ -63,7 +63,7 @@ const PhotoSharing = ( ) => {
const { data } = item;
// when sharing, we need to reset zustand like we do while
// navigating through the AddObsModal
// navigating through the AddObsBottomSheet
resetObservationFlowSlice( );
const photoUris = data

View File

@@ -18,7 +18,10 @@ const { width } = Dimensions.get( "window" );
const marginOnWide = {
marginHorizontal: width > 500
? ( width - 500 ) / 2
: 0
: 0,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
overflow: "hidden"
};
// eslint-disable-next-line
@@ -29,14 +32,15 @@ interface Props {
hidden?: boolean;
hideCloseButton?: boolean;
headerText?: string;
onLayout?: Function;
onLayout?: ( event: object ) => void;
// Callback when the user presses the close button or backdrop, not whenever the sheet
// closes
onPressClose?: Function;
onPressClose?: () => void;
snapPoints?: Array<string>;
insideModal?: boolean;
keyboardShouldPersistTaps?: string;
testID?: string;
additionalClasses?: string;
}
const StandardBottomSheet = ( {
@@ -49,6 +53,7 @@ const StandardBottomSheet = ( {
snapPoints,
insideModal,
keyboardShouldPersistTaps = "never",
additionalClasses,
testID
}: Props ): Node => {
if ( snapPoints ) {
@@ -115,21 +120,24 @@ const StandardBottomSheet = ( {
"pt-7",
insets.bottom > 0
? "pb-7"
: null
: null,
additionalClasses
)}
onLayout={onLayout}
// Not ideal, but @gorhom/bottom-sheet components don't support
// testID
testID={testID}
>
<View className="mx-12 flex">
<Heading4
testID="bottom-sheet-header"
className="w-full text-center"
>
{headerText}
</Heading4>
</View>
{headerText && (
<View className="mx-12 flex">
<Heading4
testID="bottom-sheet-header"
className="w-full text-center"
>
{headerText}
</Heading4>
</View>
)}
{children}
{!hideCloseButton && (
<INatIconButton

View File

@@ -228,9 +228,6 @@ Closes-explanation = Closes explanation
# appear when you first install the app
Closes-introduction = Closes introduction
# Accessibility hint for button that closes the help that
# appears when you start a new observation for the first time
Closes-new-observation-explanation = Closes new observation explanation.
Closes-new-observation-options = Closes new observation options.
Closes-withdraw-id-sheet = Closes "Withdraw ID" sheet
# Heading for a section that describes people and organizations that
# collaborate with iNaturalist
@@ -274,7 +271,7 @@ Couldnt-create-comment = Couldn't create comment
Couldnt-create-identification-error = Couldn't create identification { $error }
Couldnt-create-identification-unknown-error = Couldn't create identification, unknown error.
CREATE-AN-ACCOUNT = CREATE AN ACCOUNT
Create-an-observation-evidence = Create an observation with no evidence
Create-observation-with-no-evidence = Create observation with no evidence
DATA-QUALITY = DATA QUALITY
DATA-QUALITY-ASSESSMENT = DATA QUALITY ASSESSMENT
# Label for button that navigates users to the data quality screen
@@ -563,6 +560,7 @@ Iconic-taxon-name = Iconic taxon name: { $iconicTaxon }
ID-Suggestions = ID Suggestions
# Short for: Identify with AI. Label for a button that will load identifications for a given photo/sound
ID-WITH-AI = ID WITH AI
ID-with-AI-Camera = ID with AI Camera
# Identification Status
ID-Withdrawn = ID Withdrawn
IDENTIFICATION = IDENTIFICATION
@@ -575,7 +573,6 @@ IDENTIFICATIONS-WITHOUT-NUMBER =
}
Identifiers = Identifiers
Identifiers-View = Identifiers View
Identify-an-organism = Identify an organism
# Title of screen asking for permission to access the camera
Identify-organisms-in-real-time-with-your-camera = Identify organisms in real time with your camera
# Onboarding slides
@@ -925,7 +922,6 @@ POTENTIAL-DISAGREEMENT = POTENTIAL DISAGREEMENT
Potential-disagreement-description = <0>Is the evidence enough to confirm this is </0><1></1><0>?<0>
Potential-disagreement-disagree = <0>No, but this is a member of </0><1></1>
Potential-disagreement-unsure = <0>I don't know but I am sure this is </0><1></1>
Press-and-hold-to-view-more-options = Press and hold to view more options
Previous-observation = Previous observation
# Accessibility label for a button that goes to the previous slide on onboarding cards
Previous-slide = Previous slide
@@ -1210,8 +1206,8 @@ Switches-to-tab = Switches to { $tab } tab.
Sync-observations = Sync observations
Syncing = Syncing...
# Help text for the button that opens the multi-capture camera
Take-multiple-photos-of-a-single-organism = Take multiple photos of a single organism
Take-photo = Take photo
Take-photos = Take photos
# label in project requirements
Taxa = Taxa
TAXON = TAXON
@@ -1279,7 +1275,7 @@ Unreviewed-observations-only = Unreviewed observations only
Upload-Complete = Upload Complete
Upload-in-progress = Upload in progress
UPLOAD-NOW = UPLOAD NOW
Upload-photos-from-your-photo-library = Upload multiple photos from your photo library
Upload-photos = Upload photos
Upload-Progress = Upload { $uploadProgress } percent complete
UPLOAD-TO-INATURALIST = UPLOAD TO INATURALIST
# Shows the number of observations a user can upload to iNat from my observations page
@@ -1301,7 +1297,6 @@ Uploading-x-of-y-observations =
*[other] Uploading { $currentUploadCount } of { $total } observations
}
Use-iNaturalist-to-identify-any-living-thing = Use iNaturalist to identify any living thing
Use-iNaturalists-AI-Camera = Use iNaturalist's AI Camera to identify organisms in real time
# Text for a button prompting the user to grant access to location
USE-LOCATION = USE LOCATION
Use-the-devices-other-camera = Use the device's other camera.

View File

@@ -116,8 +116,6 @@
"Close-search": "Close search",
"Closes-explanation": "Closes explanation",
"Closes-introduction": "Closes introduction",
"Closes-new-observation-explanation": "Closes new observation explanation.",
"Closes-new-observation-options": "Closes new observation options.",
"Closes-withdraw-id-sheet": "Closes \"Withdraw ID\" sheet",
"COLLABORATORS": "COLLABORATORS",
"Collection-Project": "Collection Project",
@@ -144,7 +142,7 @@
"Couldnt-create-identification-error": "Couldn't create identification { $error }",
"Couldnt-create-identification-unknown-error": "Couldn't create identification, unknown error.",
"CREATE-AN-ACCOUNT": "CREATE AN ACCOUNT",
"Create-an-observation-evidence": "Create an observation with no evidence",
"Create-observation-with-no-evidence": "Create observation with no evidence",
"DATA-QUALITY": "DATA QUALITY",
"DATA-QUALITY-ASSESSMENT": "DATA QUALITY ASSESSMENT",
"Data-Quality-Assessment": "Data Quality Assessment",
@@ -315,13 +313,13 @@
"Iconic-taxon-name": "Iconic taxon name: { $iconicTaxon }",
"ID-Suggestions": "ID Suggestions",
"ID-WITH-AI": "ID WITH AI",
"ID-with-AI-Camera": "ID with AI Camera",
"ID-Withdrawn": "ID Withdrawn",
"IDENTIFICATION": "IDENTIFICATION",
"Identification-options": "Identification options",
"IDENTIFICATIONS-WITHOUT-NUMBER": "{ $count ->\n [one] IDENTIFICATION\n *[other] IDENTIFICATIONS\n}",
"Identifiers": "Identifiers",
"Identifiers-View": "Identifiers View",
"Identify-an-organism": "Identify an organism",
"Identify-organisms-in-real-time-with-your-camera": "Identify organisms in real time with your camera",
"Identify-species-anywhere": "Identify species anywhere",
"If-an-account-with-that-email-exists": "If an account with that email exists, we've sent password reset instructions to your email.",
@@ -546,7 +544,6 @@
"Potential-disagreement-description": "<0>Is the evidence enough to confirm this is </0><1></1><0>?<0>",
"Potential-disagreement-disagree": "<0>No, but this is a member of </0><1></1>",
"Potential-disagreement-unsure": "<0>I don't know but I am sure this is </0><1></1>",
"Press-and-hold-to-view-more-options": "Press and hold to view more options",
"Previous-observation": "Previous observation",
"Previous-slide": "Previous slide",
"Privacy-Policy": "Privacy Policy",
@@ -762,8 +759,8 @@
"Switches-to-tab": "Switches to { $tab } tab.",
"Sync-observations": "Sync observations",
"Syncing": "Syncing...",
"Take-multiple-photos-of-a-single-organism": "Take multiple photos of a single organism",
"Take-photo": "Take photo",
"Take-photos": "Take photos",
"Taxa": "Taxa",
"TAXON": "TAXON",
"TAXON-NAMES-DISPLAY": "TAXON NAMES DISPLAY",
@@ -818,7 +815,7 @@
"Upload-Complete": "Upload Complete",
"Upload-in-progress": "Upload in progress",
"UPLOAD-NOW": "UPLOAD NOW",
"Upload-photos-from-your-photo-library": "Upload multiple photos from your photo library",
"Upload-photos": "Upload photos",
"Upload-Progress": "Upload { $uploadProgress } percent complete",
"UPLOAD-TO-INATURALIST": "UPLOAD TO INATURALIST",
"Upload-x-observations": "Upload { $count ->\n [one] 1 observation\n *[other] { $count } observations\n}",
@@ -827,7 +824,6 @@
"Uploading-x-of-y": "Uploading { $currentUploadCount } of { $total }",
"Uploading-x-of-y-observations": "{ $total ->\n [one] Uploading { $currentUploadCount } observation\n *[other] Uploading { $currentUploadCount } of { $total } observations\n}",
"Use-iNaturalist-to-identify-any-living-thing": "Use iNaturalist to identify any living thing",
"Use-iNaturalists-AI-Camera": "Use iNaturalist's AI Camera to identify organisms in real time",
"USE-LOCATION": "USE LOCATION",
"Use-the-devices-other-camera": "Use the device's other camera.",
"Use-the-iNaturalist-camera-to-see-real-time-identifications-and-take-photos": "Use the iNaturalist camera to see real-time identifications and take photos!",

View File

@@ -228,9 +228,6 @@ Closes-explanation = Closes explanation
# appear when you first install the app
Closes-introduction = Closes introduction
# Accessibility hint for button that closes the help that
# appears when you start a new observation for the first time
Closes-new-observation-explanation = Closes new observation explanation.
Closes-new-observation-options = Closes new observation options.
Closes-withdraw-id-sheet = Closes "Withdraw ID" sheet
# Heading for a section that describes people and organizations that
# collaborate with iNaturalist
@@ -274,7 +271,7 @@ Couldnt-create-comment = Couldn't create comment
Couldnt-create-identification-error = Couldn't create identification { $error }
Couldnt-create-identification-unknown-error = Couldn't create identification, unknown error.
CREATE-AN-ACCOUNT = CREATE AN ACCOUNT
Create-an-observation-evidence = Create an observation with no evidence
Create-observation-with-no-evidence = Create observation with no evidence
DATA-QUALITY = DATA QUALITY
DATA-QUALITY-ASSESSMENT = DATA QUALITY ASSESSMENT
# Label for button that navigates users to the data quality screen
@@ -563,6 +560,7 @@ Iconic-taxon-name = Iconic taxon name: { $iconicTaxon }
ID-Suggestions = ID Suggestions
# Short for: Identify with AI. Label for a button that will load identifications for a given photo/sound
ID-WITH-AI = ID WITH AI
ID-with-AI-Camera = ID with AI Camera
# Identification Status
ID-Withdrawn = ID Withdrawn
IDENTIFICATION = IDENTIFICATION
@@ -575,7 +573,6 @@ IDENTIFICATIONS-WITHOUT-NUMBER =
}
Identifiers = Identifiers
Identifiers-View = Identifiers View
Identify-an-organism = Identify an organism
# Title of screen asking for permission to access the camera
Identify-organisms-in-real-time-with-your-camera = Identify organisms in real time with your camera
# Onboarding slides
@@ -925,7 +922,6 @@ POTENTIAL-DISAGREEMENT = POTENTIAL DISAGREEMENT
Potential-disagreement-description = <0>Is the evidence enough to confirm this is </0><1></1><0>?<0>
Potential-disagreement-disagree = <0>No, but this is a member of </0><1></1>
Potential-disagreement-unsure = <0>I don't know but I am sure this is </0><1></1>
Press-and-hold-to-view-more-options = Press and hold to view more options
Previous-observation = Previous observation
# Accessibility label for a button that goes to the previous slide on onboarding cards
Previous-slide = Previous slide
@@ -1210,8 +1206,8 @@ Switches-to-tab = Switches to { $tab } tab.
Sync-observations = Sync observations
Syncing = Syncing...
# Help text for the button that opens the multi-capture camera
Take-multiple-photos-of-a-single-organism = Take multiple photos of a single organism
Take-photo = Take photo
Take-photos = Take photos
# label in project requirements
Taxa = Taxa
TAXON = TAXON
@@ -1279,7 +1275,7 @@ Unreviewed-observations-only = Unreviewed observations only
Upload-Complete = Upload Complete
Upload-in-progress = Upload in progress
UPLOAD-NOW = UPLOAD NOW
Upload-photos-from-your-photo-library = Upload multiple photos from your photo library
Upload-photos = Upload photos
Upload-Progress = Upload { $uploadProgress } percent complete
UPLOAD-TO-INATURALIST = UPLOAD TO INATURALIST
# Shows the number of observations a user can upload to iNat from my observations page
@@ -1301,7 +1297,6 @@ Uploading-x-of-y-observations =
*[other] Uploading { $currentUploadCount } of { $total } observations
}
Use-iNaturalist-to-identify-any-living-thing = Use iNaturalist to identify any living thing
Use-iNaturalists-AI-Camera = Use iNaturalist's AI Camera to identify organisms in real time
# Text for a button prompting the user to grant access to location
USE-LOCATION = USE LOCATION
Use-the-devices-other-camera = Use the device's other camera.

View File

@@ -1,6 +1,6 @@
// @flow
import classNames from "classnames";
import AddObsButton from "components/AddObsModal/AddObsButton";
import AddObsButton from "components/AddObsBottomSheet/AddObsButton";
import { View } from "components/styledComponents";
import type { Node } from "react";
import React from "react";

View File

@@ -1,5 +1,5 @@
import { screen, userEvent } from "@testing-library/react-native";
import AddObsButton from "components/AddObsModal/AddObsButton";
import AddObsButton from "components/AddObsBottomSheet/AddObsButton";
import i18next from "i18next";
import React from "react";
import { renderComponent } from "tests/helpers/render";
@@ -48,8 +48,8 @@ const longPress = async ( ) => {
};
const showNoEvidenceOption = ( ) => {
const noEvidenceButton = screen.getByLabelText(
i18next.t( "Observation-with-no-evidence" )
const noEvidenceButton = screen.getByTestId(
i18next.t( "observe-without-evidence-button" )
);
expect( noEvidenceButton ).toBeTruthy( );
return noEvidenceButton;
@@ -87,7 +87,7 @@ describe( "with advanced user layout", ( ) => {
} );
} );
it( "opens AddObsModal", async ( ) => {
it( "opens AddObsBottomSheet", async ( ) => {
renderComponent( <AddObsButton /> );
await regularPress( );
showNoEvidenceOption( );

View File

@@ -1,11 +1,7 @@
import { screen } from "@testing-library/react-native";
import AddObsButton from "components/AddObsModal/AddObsButton";
import AddObsButton from "components/AddObsBottomSheet/AddObsButton";
import React from "react";
import * as useCurrentUser from "sharedHooks/useCurrentUser";
import { zustandStorage } from "stores/useStore";
import factory from "tests/factory";
import { renderComponent } from "tests/helpers/render";
import setStoreStateLayout from "tests/helpers/setStoreStateLayout";
// Mock getCurrentRoute to return ObsList
jest.mock( "navigation/navigationUtils", () => ( {
@@ -14,8 +10,6 @@ jest.mock( "navigation/navigationUtils", () => ( {
} )
} ) );
const mockUser = factory( "LocalUser" );
jest.mock( "sharedHooks/useCurrentUser", () => ( {
__esModule: true,
default: () => undefined
@@ -34,88 +28,4 @@ describe( "AddObsButton", () => {
// Snapshot test
expect( screen ).toMatchSnapshot();
} );
it( "does not render tooltip in default state", () => {
renderComponent( <AddObsButton /> );
const tooltipText = screen.queryByText(
"Press and hold to view more options"
);
expect( tooltipText ).toBeFalsy();
} );
} );
describe( "shows tooltip", () => {
it( "to logged out users with 2 observations", async () => {
zustandStorage.setItem( "numOfUserObservations", 2 );
renderComponent( <AddObsButton /> );
const tooltipText = await screen.findByText(
"Press and hold to view more options"
);
expect( tooltipText ).toBeTruthy();
} );
it( "to logged in users with less than 50 observations", async () => {
zustandStorage.setItem( "numOfUserObservations", 2 );
jest.spyOn( useCurrentUser, "default" ).mockImplementation( () => mockUser );
renderComponent( <AddObsButton /> );
// Temporarily disabled the tooltip for new users, as it is freezing the app in some cases.
// const tooltipText = await screen.findByText(
// "Press and hold to view more options"
// );
// expect( tooltipText ).toBeTruthy();
} );
it( "to new users only after they dismissed the account creation card", async () => {
zustandStorage.setItem( "numOfUserObservations", 1 );
setStoreStateLayout( {
justFinishedSignup: true
} );
renderComponent( <AddObsButton /> );
const tooltipText = screen.queryByText(
"Press and hold to view more options"
);
expect( tooltipText ).toBeFalsy();
setStoreStateLayout( {
shownOnce: {
"account-creation": true
}
} );
// Temporarily disabled the tooltip for new users, as it is freezing the app in some cases.
// const tooltipTextAfter = await screen.findByText(
// "Press and hold to view more options"
// );
// expect( tooltipTextAfter ).toBeTruthy();
} );
it( "to logged in users with more than 50 observations after card dismissal", async () => {
zustandStorage.setItem( "numOfUserObservations", 51 );
renderComponent( <AddObsButton /> );
const tooltipText = screen.queryByText(
"Press and hold to view more options"
);
expect( tooltipText ).toBeFalsy();
setStoreStateLayout( {
shownOnce: {
"fifty-observation": true
}
} );
// Temporarily disabled the tooltip for new users, as it is freezing the app in some cases.
// const tooltipTextAfter = await screen.findByText(
// "Press and hold to view more options"
// );
// expect( tooltipTextAfter ).toBeTruthy();
} );
} );

View File

@@ -1,6 +1,5 @@
import { render, screen } from "@testing-library/react-native";
import AddObsModal from "components/AddObsModal/AddObsModal";
import i18next from "i18next";
import AddObsBottomSheet from "components/AddObsBottomSheet/AddObsBottomSheet";
import React from "react";
// Make sure the mock is using a recent-ish version
@@ -13,11 +12,11 @@ jest.mock( "react-native/Libraries/Utilities/Platform", ( ) => ( {
}
} ) );
describe( "AddObsModal", ( ) => {
describe( "AddObsBottomSheet", ( ) => {
it( "shows the AI camera button", async ( ) => {
render( <AddObsModal closeModal={jest.fn( )} /> );
const aiCameraButton = screen.getByLabelText(
i18next.t( "AI-Camera" )
render( <AddObsBottomSheet closeModal={jest.fn( )} /> );
const aiCameraButton = screen.getByTestId(
"aicamera-button"
);
expect( aiCameraButton ).toBeOnTheScreen();
} );

View File

@@ -1,8 +1,8 @@
// Separate tests for iOS 9. AddObsModal sets some OS-specific constants at
// Separate tests for iOS 9. AddObsBottomSheet sets some OS-specific constants at
// load time that can't be altered at runtime, so we're using a separate test
// with a separate mock to control those load time values.
import { render, screen } from "@testing-library/react-native";
import AddObsModal from "components/AddObsModal/AddObsModal";
import AddObsBottomSheet from "components/AddObsBottomSheet/AddObsBottomSheet";
import i18next from "i18next";
import React from "react";
@@ -16,9 +16,9 @@ jest.mock( "react-native/Libraries/Utilities/Platform", () => ( {
}
} ) );
describe( "AddObsModal in iOS 9", ( ) => {
describe( "AddObsBottomSheet in iOS 9", ( ) => {
it( "hides AI camera button on older devices", async ( ) => {
render( <AddObsModal closeModal={jest.fn( )} /> );
render( <AddObsBottomSheet closeModal={jest.fn( )} /> );
const arCameraButton = screen.queryByLabelText(
i18next.t( "AI-Camera" )
);