Advanced settings UI updates (#2797)

* Open more options on long press

* Add tests for long press

* Rearranging Settings screen with new layout

* Add toggle for advanced settings in layout slice; fix default mode toggle

* Update settings with navigation flows

* Fix tests

* Change power mode switch for e2e test

* Fix settings test for green button toggle

* Fix advanced user toggle in e2e test (which hides pivot cards)

* Changes based on design convo; test fixes

* Fix e2e tests

* Follow user flow chart and update nav accordingly

* Rename function

* Fix test

* Can be null so check for false only

* Little less spacing between radio button rows

* Minor UI updates

* Remove check for previous setting in UI

* This is no longer used anywhere

* Update AICamera.test.js

* Update AICamera.test.js

* Update AICamera.test.js

* Update Suggestions.test.js

* Update Settings.test.js

* Update LanguageSettings.test.js

---------

Co-authored-by: Johannes Klein <johannes.t.klein@gmail.com>
This commit is contained in:
Amanda Bullington
2025-03-27 09:36:36 -07:00
committed by GitHub
parent b957416abe
commit d782538109
38 changed files with 623 additions and 450 deletions

View File

@@ -10,16 +10,8 @@ export default async function switchPowerMode() {
const settingsDrawerMenuItem = element( by.id( "settings" ) );
await waitFor( settingsDrawerMenuItem ).toBeVisible().withTimeout( 10000 );
await settingsDrawerMenuItem.tap();
// Tap the settings radio button for advanced interface mode
const advancedInterfaceRadioButton = element( by.id( "advanced-interface-option" ) );
await waitFor( advancedInterfaceRadioButton ).toBeVisible().withTimeout( 10000 );
await advancedInterfaceRadioButton.tap();
// Tap the settings radio button for power user mode
const powerUserRadioButton = element( by.id( "all-observation-options" ) );
await waitFor( powerUserRadioButton ).toBeVisible().withTimeout( 10000 );
await powerUserRadioButton.tap();
// Tap the settings radio button for suggestions flow first mode
const suggestionsFlowButton = element( by.id( "suggestions-flow-mode" ) );
await waitFor( suggestionsFlowButton ).toBeVisible().withTimeout( 10000 );
await suggestionsFlowButton.tap();
// Switch settings to advanced interface mode
const advancedInterfaceSwitch = element( by.id( "advanced-interface-switch.switch" ) );
await waitFor( advancedInterfaceSwitch ).toBeVisible().withTimeout( 10000 );
await advancedInterfaceSwitch.tap();
}

View File

@@ -15,7 +15,7 @@ import type {
} from "react-native-vision-camera";
import { createSentinelFile, deleteSentinelFile, logStage } from "sharedHelpers/sentinelFiles.ts";
import {
useDeviceOrientation, useLayoutPrefs, useTranslation, useWatchPosition
useDeviceOrientation, useTranslation, useWatchPosition
} from "sharedHooks";
import useLocationPermission from "sharedHooks/useLocationPermission.tsx";
import useStore from "stores/useStore";
@@ -29,9 +29,6 @@ import useSavePhotoPermission from "./hooks/useSavePhotoPermission";
export const MAX_PHOTOS_ALLOWED = 20;
const CameraContainer = ( ) => {
const {
isDefaultMode
} = useLayoutPrefs( );
const currentObservation = useStore( state => state.currentObservation );
const setCameraState = useStore( state => state.setCameraState );
const evidenceToAdd = useStore( state => state.evidenceToAdd );
@@ -43,11 +40,6 @@ const CameraContainer = ( ) => {
const { params } = useRoute( );
const cameraType = params?.camera;
const showMatchScreen = cameraType === "AI"
&& isDefaultMode;
const showSuggestionsScreen = cameraType === "AI"
&& !isDefaultMode;
const logStageIfAICamera = useCallback( async (
stageName: string,
stageData: string
@@ -152,16 +144,14 @@ const CameraContainer = ( ) => {
newPhotoState,
logStageIfAICamera,
deleteStageIfAICamera,
showMatchScreen,
showSuggestionsScreen
cameraType
} );
}, [
prepareStoreAndNavigate,
navigationOptions,
logStageIfAICamera,
deleteStageIfAICamera,
showMatchScreen,
showSuggestionsScreen
cameraType
] );
const handleCheckmarkPress = useCallback( async newPhotoState => {

View File

@@ -6,6 +6,9 @@ import {
import Observation from "realmModels/Observation";
import ObservationPhoto from "realmModels/ObservationPhoto";
import fetchPlaceName from "sharedHelpers/fetchPlaceName";
import {
useLayoutPrefs
} from "sharedHooks";
import useStore from "stores/useStore";
import savePhotosToPhotoLibrary from "../helpers/savePhotosToPhotoLibrary";
@@ -25,7 +28,7 @@ const usePrepareStoreAndNavigate = ( ): Function => {
const setSavingPhoto = useStore( state => state.setSavingPhoto );
const setCameraState = useStore( state => state.setCameraState );
const setSentinelFileName = useStore( state => state.setSentinelFileName );
const isAdvancedSuggestionsMode = useStore( state => state.layout.isAdvancedSuggestionsMode );
const { screenAfterPhotoEvidence, isDefaultMode } = useLayoutPrefs( );
const { deviceStorageFull, showStorageFullAlert } = useDeviceStorageFull( );
@@ -137,8 +140,7 @@ const usePrepareStoreAndNavigate = ( ): Function => {
newPhotoState,
logStageIfAICamera,
deleteStageIfAICamera,
showMatchScreen,
showSuggestionsScreen
cameraType
} ) => {
if ( userLocation !== null ) {
logStageIfAICamera( "fetch_user_location_complete" );
@@ -162,19 +164,29 @@ const usePrepareStoreAndNavigate = ( ): Function => {
await deleteStageIfAICamera( );
setSentinelFileName( null );
if ( showMatchScreen ) {
return navigation.push( "Match", {
entryScreen: "CameraWithDevice",
lastScreen: "CameraWithDevice"
} );
}
if ( showSuggestionsScreen || isAdvancedSuggestionsMode ) {
// AI camera can only go to Match/Suggestions
if ( cameraType === "AI" ) {
if ( isDefaultMode ) {
return navigation.push( "Match", {
entryScreen: "CameraWithDevice",
lastScreen: "CameraWithDevice"
} );
}
return navigation.push( "Suggestions", {
entryScreen: "CameraWithDevice",
lastScreen: "CameraWithDevice"
} );
}
return navigation.push( "ObsEdit", {
// Multicapture camera in default mode should only go to Match screen
if ( isDefaultMode ) {
return navigation.push( "Match", {
entryScreen: "CameraWithDevice",
lastScreen: "CameraWithDevice"
} );
}
// Multicapture camera navigates based on user settings to Match, Suggestions, or ObsEdit
return navigation.push( screenAfterPhotoEvidence, {
entryScreen: "CameraWithDevice",
lastScreen: "CameraWithDevice"
} );
@@ -185,7 +197,8 @@ const usePrepareStoreAndNavigate = ( ): Function => {
setSentinelFileName,
navigation,
updateObsWithCameraPhotos,
isAdvancedSuggestionsMode
screenAfterPhotoEvidence,
isDefaultMode
] );
return prepareStoreAndNavigate;

View File

@@ -25,7 +25,7 @@ type Props = {
combinePhotos: Function,
groupedPhotos: Array<Object>,
isCreatingObservations?: boolean,
navToObsEditOrSuggestions: Function,
navBasedOnUserSettings: Function,
removePhotos: Function,
selectedObservations: Array<Object>,
selectObservationPhotos: Function,
@@ -37,7 +37,7 @@ const GroupPhotos = ( {
combinePhotos,
groupedPhotos,
isCreatingObservations,
navToObsEditOrSuggestions,
navBasedOnUserSettings,
removePhotos,
selectedObservations,
selectObservationPhotos,
@@ -193,7 +193,7 @@ const GroupPhotos = ( {
className="max-w-[500px] w-full"
level="focus"
text={t( "IMPORT-X-OBSERVATIONS", { count: groupedPhotos.length } )}
onPress={navToObsEditOrSuggestions}
onPress={navBasedOnUserSettings}
testID="GroupPhotos.next"
loading={isCreatingObservations}
/>

View File

@@ -5,6 +5,7 @@ import { t } from "i18next";
import type { Node } from "react";
import React, { useEffect, useState } from "react";
import Observation from "realmModels/Observation";
import { useLayoutPrefs } from "sharedHooks";
import useStore from "stores/useStore";
import GroupPhotos from "./GroupPhotos";
@@ -12,6 +13,9 @@ import flattenAndOrderSelectedPhotos from "./helpers/groupPhotoHelpers";
const GroupPhotosContainer = ( ): Node => {
const navigation = useNavigation( );
const {
screenAfterPhotoEvidence, isDefaultMode
} = useLayoutPrefs( );
const setObservations = useStore( state => state.setObservations );
const setGroupedPhotos = useStore( state => state.setGroupedPhotos );
const groupedPhotos = useStore( state => state.groupedPhotos );
@@ -130,7 +134,7 @@ const GroupPhotosContainer = ( ): Node => {
setSelectedObservations( [] );
};
const navToObsEditOrSuggestions = async ( ) => {
const navBasedOnUserSettings = async ( ) => {
setIsCreatingObservations( true );
const newObservations = await Promise.all( groupedPhotos.map(
( { photos } ) => Observation.createObservationWithPhotos( photos )
@@ -145,10 +149,26 @@ const GroupPhotosContainer = ( ): Node => {
} ) ) );
setIsCreatingObservations( false );
if ( newObservations.length === 1 ) {
navigation.push( "Suggestions", { entryScreen: "GroupPhotos", lastScreen: "GroupPhotos" } );
} else {
navigation.navigate( "ObsEdit", { lastScreen: "GroupPhotos" } );
if ( isDefaultMode ) {
return navigation.navigate( "NoBottomTabStackNavigator", {
screen: "Match",
params: {
entryScreen: "GroupPhotos",
lastScreen: "GroupPhotos"
}
} );
}
// in advanced mode, navigate based on user preference
return navigation.navigate( "NoBottomTabStackNavigator", {
screen: screenAfterPhotoEvidence,
params: {
entryScreen: "GroupPhotos",
lastScreen: "GroupPhotos"
}
} );
}
return navigation.navigate( "ObsEdit", { lastScreen: "GroupPhotos" } );
};
return (
@@ -156,7 +176,7 @@ const GroupPhotosContainer = ( ): Node => {
combinePhotos={combinePhotos}
groupedPhotos={groupedPhotos}
isCreatingObservations={isCreatingObservations}
navToObsEditOrSuggestions={navToObsEditOrSuggestions}
navBasedOnUserSettings={navBasedOnUserSettings}
removePhotos={removePhotos}
selectObservationPhotos={selectObservationPhotos}
selectedObservations={selectedObservations}

View File

@@ -30,7 +30,7 @@ const DEFAULT_MODE_MAX_PHOTOS_ALLOWED = 1;
const PhotoLibrary = ( ): Node => {
const {
isDefaultMode
screenAfterPhotoEvidence, isDefaultMode
} = useLayoutPrefs( );
const navigation = useNavigation( );
const [photoLibraryShown, setPhotoLibraryShown] = useState( false );
@@ -44,7 +44,6 @@ const PhotoLibrary = ( ): Node => {
const currentObservationIndex = useStore( state => state.currentObservationIndex );
const observations = useStore( state => state.observations );
const numOfObsPhotos = currentObservation?.observationPhotos?.length || 0;
const isAdvancedSuggestionsMode = useStore( state => state.layout.isAdvancedSuggestionsMode );
const exitObservationsFlow = useExitObservationsFlow( );
const { params } = useRoute( );
@@ -54,30 +53,29 @@ const PhotoLibrary = ( ): Node => {
const fromGroupPhotos = params
? params.fromGroupPhotos
: false;
const lastScreen = params?.lastScreen;
const navToObsEdit = useCallback( ( ) => navigation.navigate( "ObsEdit", {
lastScreen: "PhotoLibrary"
} ), [navigation] );
const advanceToMatchScreen = lastScreen === "Camera"
&& isDefaultMode;
const navBasedOnUserSettings = useCallback( async ( ) => {
if ( advanceToMatchScreen ) {
return navigation.navigate( "Match", {
lastScreen: "PhotoLibrary"
if ( isDefaultMode ) {
return navigation.navigate( "NoBottomTabStackNavigator", {
screen: "Match",
params: {
lastScreen: "PhotoLibrary"
}
} );
}
if ( isAdvancedSuggestionsMode ) {
return navigation.navigate( "Suggestions", {
// in advanced mode, navigate based on user preference
return navigation.navigate( "NoBottomTabStackNavigator", {
screen: screenAfterPhotoEvidence,
params: {
lastScreen: "PhotoLibrary"
} );
}
return navigation.navigate( "ObsEdit", {
lastScreen: "PhotoLibrary"
}
} );
}, [navigation, advanceToMatchScreen, isAdvancedSuggestionsMode] );
}, [navigation, screenAfterPhotoEvidence, isDefaultMode] );
const moveImagesToDocumentsDirectory = async selectedImages => {
const path = photoLibraryPhotosPath;
@@ -116,7 +114,7 @@ const PhotoLibrary = ( ): Node => {
// According to the native code of the image picker library, it never rejects the promise,
// just returns a response object with errorCode
const response = await ImagePicker.launchImageLibrary( {
selectionLimit: advanceToMatchScreen
selectionLimit: screenAfterPhotoEvidence === "Match"
? DEFAULT_MODE_MAX_PHOTOS_ALLOWED
: MAX_PHOTOS_ALLOWED,
mediaType: "photo",
@@ -217,7 +215,6 @@ const PhotoLibrary = ( ): Node => {
setPhotoLibraryShown( false );
}
}, [
advanceToMatchScreen,
currentObservation,
currentObservationIndex,
evidenceToAdd,
@@ -232,6 +229,7 @@ const PhotoLibrary = ( ): Node => {
observations,
params,
photoLibraryShown,
screenAfterPhotoEvidence,
setGroupedPhotos,
setPhotoImporterState,
skipGroupPhotos,

View File

@@ -18,36 +18,44 @@ const PhotoSharing = ( ): Node => {
const resetObservationFlowSlice = useStore( state => state.resetObservationFlowSlice );
const prepareObsEdit = useStore( state => state.prepareObsEdit );
const setPhotoImporterState = useStore( state => state.setPhotoImporterState );
const { isAdvancedSuggestionsMode, isDefaultMode } = useLayoutPrefs();
const { screenAfterPhotoEvidence, isDefaultMode } = useLayoutPrefs();
const [navigationHandled, setNavigationHandled] = useState( null );
const createObservationAndNavToObsEdit = useCallback( async photoUris => {
const createObservationAndNavigate = useCallback( async photoUris => {
try {
const newObservation = await Observation.createObservationWithPhotos( photoUris );
newObservation.description = sharedText;
prepareObsEdit( newObservation );
if ( isDefaultMode ) {
navigation.navigate( "NoBottomTabStackNavigator", { screen: "Match" } );
} else if ( isAdvancedSuggestionsMode ) {
navigation.navigate(
"NoBottomTabStackNavigator",
{ screen: "Suggestions", params: { lastScreen: "PhotoSharing" } }
);
} else {
navigation.navigate( "NoBottomTabStackNavigator", { screen: "ObsEdit" } );
return navigation.navigate( "NoBottomTabStackNavigator", {
screen: "Match",
params: {
lastScreen: "PhotoSharing"
}
} );
}
// in advanced mode, navigate based on user preference
return navigation.navigate( "NoBottomTabStackNavigator", {
screen: screenAfterPhotoEvidence,
params: {
lastScreen: "PhotoSharing"
}
} );
} catch ( e ) {
Alert.alert(
"Photo sharing failed: couldn't create new observation:",
e
);
return null;
}
}, [
isDefaultMode,
navigation,
prepareObsEdit,
sharedText,
isAdvancedSuggestionsMode,
isDefaultMode
screenAfterPhotoEvidence
] );
useEffect( ( ) => {
@@ -83,8 +91,7 @@ const PhotoSharing = ( ): Node => {
}
if ( photoUris.length === 1 ) {
// Only one photo - go to ObsEdit directly
createObservationAndNavToObsEdit( photoUris );
createObservationAndNavigate( photoUris );
} else {
// Go to GroupPhotos screen
const firstObservationDefaults = { description: sharedText };
@@ -98,7 +105,7 @@ const PhotoSharing = ( ): Node => {
navigation.navigate( "NoBottomTabStackNavigator", { screen: "GroupPhotos" } );
}
}, [
createObservationAndNavToObsEdit,
createObservationAndNavigate,
item,
navigation,
resetObservationFlowSlice,

View File

@@ -0,0 +1,77 @@
import {
Body2,
RadioButtonRow
} from "components/SharedComponents";
import React from "react";
import {
View
} from "react-native";
import {
useLayoutPrefs,
useTranslation
} from "sharedHooks";
import { SCREEN_AFTER_PHOTO_EVIDENCE } from "stores/createLayoutSlice.ts";
const AdvancedSettings = ( ) => {
const { t } = useTranslation();
const {
isAllAddObsOptionsMode,
setIsAllAddObsOptionsMode,
screenAfterPhotoEvidence,
setScreenAfterPhotoEvidence
} = useLayoutPrefs();
const renderSettingDescription = description => (
<Body2>{description}</Body2>
);
return (
<>
<View className="mt-[20px]">
{renderSettingDescription( t( "When-tapping-the-green-observation-button" ) )}
<RadioButtonRow
classNames="ml-[6px] mt-[15px]"
testID="all-observation-options"
smallLabel
checked={isAllAddObsOptionsMode}
onPress={() => setIsAllAddObsOptionsMode( true )}
label={t( "All-observation-options--list" )}
/>
<RadioButtonRow
classNames="ml-[6px] mt-[15px]"
smallLabel
checked={!isAllAddObsOptionsMode}
onPress={() => setIsAllAddObsOptionsMode( false )}
label={t( "iNaturalist-AI-Camera" )}
/>
</View>
<View className="mt-[20px]">
{renderSettingDescription( t( "After-capturing-or-importing-photos-show" ) )}
<RadioButtonRow
classNames="ml-[6px] mt-[15px]"
testID="suggestions-flow-mode"
smallLabel
checked={screenAfterPhotoEvidence === SCREEN_AFTER_PHOTO_EVIDENCE.SUGGESTIONS}
onPress={() => setScreenAfterPhotoEvidence( SCREEN_AFTER_PHOTO_EVIDENCE.SUGGESTIONS )}
label={t( "ID-Suggestions" )}
/>
<RadioButtonRow
classNames="ml-[6px] mt-[15px]"
smallLabel
checked={screenAfterPhotoEvidence === SCREEN_AFTER_PHOTO_EVIDENCE.OBS_EDIT}
onPress={() => setScreenAfterPhotoEvidence( SCREEN_AFTER_PHOTO_EVIDENCE.OBS_EDIT )}
label={t( "Edit-Observation" )}
/>
<RadioButtonRow
classNames="ml-[6px] mt-[15px]"
smallLabel
checked={screenAfterPhotoEvidence === SCREEN_AFTER_PHOTO_EVIDENCE.MATCH}
onPress={() => setScreenAfterPhotoEvidence( SCREEN_AFTER_PHOTO_EVIDENCE.MATCH )}
label={t( "Match-Screen" )}
/>
</View>
</>
);
};
export default AdvancedSettings;

View File

@@ -0,0 +1,188 @@
import {
useNetInfo
} from "@react-native-community/netinfo";
import { useFocusEffect, useNavigation } from "@react-navigation/native";
import { useQueryClient } from "@tanstack/react-query";
import {
signOut
} from "components/LoginSignUp/AuthenticationService.ts";
import {
ActivityIndicator,
Body2,
Button,
Heading4
} from "components/SharedComponents";
import { RealmContext } from "providers/contexts.ts";
import React, { useCallback, useEffect, useState } from "react";
import {
Alert,
View
} from "react-native";
import Config from "react-native-config";
import { EventRegister } from "react-native-event-listeners";
import QueueItem from "realmModels/QueueItem.ts";
import {
useLayoutPrefs,
useTranslation,
useUserMe
} from "sharedHooks";
import LanguageSetting from "./LanguageSetting";
import TaxonNamesSetting from "./TaxonNamesSetting";
const { useRealm } = RealmContext;
const SETTINGS_URL = `${Config.OAUTH_API_URL}/users/edit?noh1=true`;
const FINISHED_WEB_SETTINGS = "finished-web-settings";
const LoggedInDefaultSettings = ( ) => {
const realm = useRealm( );
const { isConnected } = useNetInfo( );
const navigation = useNavigation( );
const { t } = useTranslation( );
const {
remoteUser, isLoading, refetchUserMe
} = useUserMe( { updateRealm: false } );
const {
setIsDefaultMode
} = useLayoutPrefs();
const [settings, setSettings] = useState( {} );
const [isSaving, setIsSaving] = useState( false );
const [showingWebViewSettings, setShowingWebViewSettings] = useState( false );
useFocusEffect(
useCallback( () => {
if ( showingWebViewSettings ) {
// When we get back from the webview of settings - in case the user updated their profile
// photo or other details
refetchUserMe();
setShowingWebViewSettings( false );
}
}, [showingWebViewSettings, refetchUserMe] )
);
const confirmInternetConnection = useCallback( ( ) => {
if ( !isConnected ) {
Alert.alert(
t( "Internet-Connection-Required" ),
t( "Please-try-again-when-you-are-connected-to-the-internet" )
);
}
return isConnected;
}, [t, isConnected] );
const queryClient = useQueryClient();
useEffect( () => {
if ( remoteUser ) {
// logger.info( remoteUser, "remote user fetched in Settings" );
setSettings( remoteUser );
setIsSaving( false );
}
}, [remoteUser, realm] );
// Listen for the webview to finish so we can fetch the updates users/me
// response
useEffect( ( ) => {
const listener = EventRegister.addEventListener(
FINISHED_WEB_SETTINGS,
refetchUserMe
);
return ( ) => {
EventRegister?.removeEventListener( listener );
};
}, [refetchUserMe] );
return (
<View className="mt-[30px]">
{( isSaving || isLoading ) && (
<View className="absolute z-10 bg-white/80
w-full h-full flex items-center justify-center"
>
<ActivityIndicator size={50} />
</View>
)}
<TaxonNamesSetting
onChange={options => {
// logger.info( "Enqueuing taxon name change with options:", options );
// logger.info( `Current user ID being updated: ${settings.id}` );
const payload = JSON.stringify( {
id: settings.id,
user: {
prefers_common_names: options.prefers_common_names,
prefers_scientific_name_first: options.prefers_scientific_name_first
}
} );
// log.info( `Payload to be enqueued: ${payload}` );
QueueItem.enqueue(
realm,
payload,
"taxon-names-change"
);
}}
/>
<LanguageSetting
onChange={newLocale => {
QueueItem.enqueue(
realm,
JSON.stringify( {
id: settings.id,
user: {
locale: newLocale
}
} ),
"locale-change"
);
}}
/>
<View>
<Heading4>{t( "INATURALIST-ACCOUNT-SETTINGS" )}</Heading4>
<Body2 className="mt-2">{t( "Edit-your-profile-change-your-settings" )}</Body2>
<Button
className="mt-4"
text={t( "ACCOUNT-SETTINGS" )}
onPress={() => {
confirmInternetConnection( );
if ( !isConnected ) { return; }
setShowingWebViewSettings( true );
navigation.navigate( "FullPageWebView", {
title: t( "ACCOUNT-SETTINGS" ),
loggedIn: true,
initialUrl: SETTINGS_URL,
blurEvent: FINISHED_WEB_SETTINGS,
clickablePathnames: ["/users/delete"],
skipSetSourceInShouldStartLoadWithRequest: true,
shouldLoadUrl: url => {
async function signOutGoHome() {
Alert.alert(
t( "Account-Deleted" ),
t( "It-may-take-up-to-an-hour-to-remove-content" )
);
// sign out
await signOut( { realm, clearRealm: true, queryClient } );
// revert back to default mode
setIsDefaultMode( true );
// navigate to My Obs
navigation.navigate( "ObsList" );
}
// If the webview navigates to a URL that indicates the account
// was deleted, sign the current user out of the app
if ( url === `${Config.OAUTH_API_URL}/?account_deleted=true` ) {
signOutGoHome( );
return false;
}
return true;
}
} );
}}
accessibilityLabel={t( "INATURALIST-SETTINGS" )}
/>
</View>
</View>
);
};
export default LoggedInDefaultSettings;

View File

@@ -1,281 +1,53 @@
import {
useNetInfo
} from "@react-native-community/netinfo";
import { useFocusEffect, useNavigation } from "@react-navigation/native";
import { useQueryClient } from "@tanstack/react-query";
import {
signOut
} from "components/LoginSignUp/AuthenticationService.ts";
import {
ActivityIndicator,
Body2,
Button,
Heading4,
RadioButtonRow,
ScrollViewWrapper
ScrollViewWrapper,
SwitchRow
} from "components/SharedComponents";
import { RealmContext } from "providers/contexts.ts";
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback } from "react";
import {
Alert,
StatusBar,
View
} from "react-native";
import Config from "react-native-config";
import { EventRegister } from "react-native-event-listeners";
import QueueItem from "realmModels/QueueItem.ts";
// import { log } from "sharedHelpers/logger";
import {
useCurrentUser,
useLayoutPrefs,
useTranslation,
useUserMe
useTranslation
} from "sharedHooks";
import LanguageSetting from "./LanguageSetting";
import TaxonNamesSetting from "./TaxonNamesSetting";
const { useRealm } = RealmContext;
const SETTINGS_URL = `${Config.OAUTH_API_URL}/users/edit?noh1=true`;
const FINISHED_WEB_SETTINGS = "finished-web-settings";
// const logger = log.extend( "Settings" );
import AdvancedSettings from "./AdvancedSettings";
import LoggedInDefaultSettings from "./LoggedInDefaultSettings";
const Settings = ( ) => {
const realm = useRealm( );
const { isConnected } = useNetInfo( );
const navigation = useNavigation( );
const { t } = useTranslation();
const currentUser = useCurrentUser( );
const {
remoteUser, isLoading, refetchUserMe
} = useUserMe( { updateRealm: false } );
const {
isDefaultMode,
isAllAddObsOptionsMode,
setIsDefaultMode,
setIsAllAddObsOptionsMode,
isAdvancedSuggestionsMode,
setIsSuggestionsFlowMode
} = useLayoutPrefs();
const [settings, setSettings] = useState( {} );
const [isSaving, setIsSaving] = useState( false );
const [showingWebViewSettings, setShowingWebViewSettings] = useState( false );
setIsDefaultMode
} = useLayoutPrefs( );
useFocusEffect(
useCallback( () => {
if ( showingWebViewSettings ) {
// When we get back from the webview of settings - in case the user updated their profile
// photo or other details
refetchUserMe();
setShowingWebViewSettings( false );
}
}, [showingWebViewSettings, refetchUserMe] )
);
const handleValueChange = useCallback( newValue => {
setIsDefaultMode( !newValue );
}, [setIsDefaultMode] );
const confirmInternetConnection = useCallback( ( ) => {
if ( !isConnected ) {
Alert.alert(
t( "Internet-Connection-Required" ),
t( "Please-try-again-when-you-are-connected-to-the-internet" )
);
}
return isConnected;
}, [t, isConnected] );
const queryClient = useQueryClient();
useEffect( () => {
if ( remoteUser ) {
// logger.info( remoteUser, "remote user fetched in Settings" );
setSettings( remoteUser );
setIsSaving( false );
}
}, [remoteUser, realm] );
// Listen for the webview to finish so we can fetch the updates users/me
// response
useEffect( ( ) => {
const listener = EventRegister.addEventListener(
FINISHED_WEB_SETTINGS,
refetchUserMe
);
return ( ) => {
EventRegister?.removeEventListener( listener );
};
}, [refetchUserMe] );
const renderLoggedIn = ( ) => (
<View>
{( isSaving || isLoading ) && (
<View className="absolute z-10 bg-white/80
w-full h-full flex items-center justify-center"
>
<ActivityIndicator size={50} />
</View>
)}
<TaxonNamesSetting
onChange={options => {
// logger.info( "Enqueuing taxon name change with options:", options );
// logger.info( `Current user ID being updated: ${settings.id}` );
const payload = JSON.stringify( {
id: settings.id,
user: {
prefers_common_names: options.prefers_common_names,
prefers_scientific_name_first: options.prefers_scientific_name_first
}
} );
// log.info( `Payload to be enqueued: ${payload}` );
QueueItem.enqueue(
realm,
payload,
"taxon-names-change"
);
}}
/>
<LanguageSetting
onChange={newLocale => {
QueueItem.enqueue(
realm,
JSON.stringify( {
id: settings.id,
user: {
locale: newLocale
}
} ),
"locale-change"
);
}}
/>
<View>
<Heading4>{t( "INATURALIST-ACCOUNT-SETTINGS" )}</Heading4>
<Body2 className="mt-2">{t( "Edit-your-profile-change-your-settings" )}</Body2>
<Button
className="mt-4"
text={t( "ACCOUNT-SETTINGS" )}
onPress={() => {
confirmInternetConnection( );
if ( !isConnected ) { return; }
setShowingWebViewSettings( true );
navigation.navigate( "FullPageWebView", {
title: t( "ACCOUNT-SETTINGS" ),
loggedIn: true,
initialUrl: SETTINGS_URL,
blurEvent: FINISHED_WEB_SETTINGS,
clickablePathnames: ["/users/delete"],
skipSetSourceInShouldStartLoadWithRequest: true,
shouldLoadUrl: url => {
async function signOutGoHome() {
Alert.alert(
t( "Account-Deleted" ),
t( "It-may-take-up-to-an-hour-to-remove-content" )
);
// sign out
await signOut( { realm, clearRealm: true, queryClient } );
// revert back to default mode
setIsDefaultMode( true );
// navigate to My Obs
navigation.navigate( "ObsList" );
}
// If the webview navigates to a URL that indicates the account
// was deleted, sign the current user out of the app
if ( url === `${Config.OAUTH_API_URL}/?account_deleted=true` ) {
signOutGoHome( );
return false;
}
return true;
}
} );
}}
accessibilityLabel={t( "INATURALIST-SETTINGS" )}
/>
</View>
</View>
);
// maybe there's a less confusing way to do this,
// but this worked for my brain on a deadline
const isAdvancedMode = !isDefaultMode;
return (
<ScrollViewWrapper>
<StatusBar barStyle="dark-content" />
<View className="p-5">
<View className="mb-9">
<Heading4>{t( "INATURALIST-MODE" )}</Heading4>
<View className="mt-[22px]">
<RadioButtonRow
smallLabel
checked={isDefaultMode}
onPress={( ) => {
setIsDefaultMode( true );
setIsAllAddObsOptionsMode( false );
}}
label={t( "Default--interface-mode" )}
/>
</View>
<View className="mt-4">
<RadioButtonRow
testID="advanced-interface-option"
smallLabel
checked={!isDefaultMode}
onPress={( ) => {
setIsDefaultMode( false );
setIsAllAddObsOptionsMode( true );
}}
label={t( "Advanced--interface-mode-with-explainer" )}
/>
</View>
</View>
{!isDefaultMode && (
<View className="mb-9">
<Heading4>{t( "OBSERVATION-BUTTON" )}</Heading4>
<Body2 className="mt-3">{t( "When-tapping-the-green-observation-button" )}</Body2>
<View className="mt-[22px] pr-5">
<RadioButtonRow
smallLabel
checked={!isAllAddObsOptionsMode}
onPress={() => setIsAllAddObsOptionsMode( false )}
label={t( "iNaturalist-AI-Camera" )}
/>
</View>
<View className="mt-4 pr-5">
<RadioButtonRow
testID="all-observation-options"
smallLabel
checked={isAllAddObsOptionsMode}
onPress={() => setIsAllAddObsOptionsMode( true )}
label={t( "All-observation-options" )}
/>
</View>
</View>
)}
{!isDefaultMode && (
<View className="mb-9">
<Heading4>{t( "SUGGESTIONS" )}</Heading4>
<Body2 className="mt-3">
{t( "After-capturing-or-importing-photos-show" )}
</Body2>
<View className="mt-4 pr-5">
<RadioButtonRow
testID="suggestions-flow-mode"
smallLabel
checked={isAdvancedSuggestionsMode}
onPress={() => setIsSuggestionsFlowMode( true )}
label={t( "ID-Suggestions" )}
/>
</View>
<View className="mt-[22px] pr-5">
<RadioButtonRow
smallLabel
checked={!isAdvancedSuggestionsMode}
onPress={() => setIsSuggestionsFlowMode( false )}
label={t( "Edit-Observation" )}
/>
</View>
</View>
)}
{currentUser && renderLoggedIn()}
<View className="p-4">
<Heading4 className="mb-[15px]">{t( "ADVANCED-SETTINGS" )}</Heading4>
<SwitchRow
testID="advanced-interface-switch"
classNames="ml-[6px]"
smallLabel
value={isAdvancedMode}
onValueChange={handleValueChange}
label={t( "View-Advanced-Settings" )}
/>
{isAdvancedMode && <AdvancedSettings />}
{currentUser && <LoggedInDefaultSettings />}
</View>
</ScrollViewWrapper>
);

View File

@@ -12,7 +12,6 @@ import {
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
import {
useCurrentUser,
useLayoutPrefs,
useTranslation
} from "sharedHooks";
@@ -35,9 +34,6 @@ const TaxonNamesSetting = ( { onChange }: Props ) => {
const realm = useRealm( );
const { t } = useTranslation( );
const currentUser = useCurrentUser( );
const {
isDefaultMode
} = useLayoutPrefs();
const changeTaxonNameDisplay = useCallback( nameDisplayPref => {
const options = {};
@@ -69,7 +65,7 @@ const TaxonNamesSetting = ( { onChange }: Props ) => {
return currentUser;
}, [currentUser, realm, onChange] );
if ( isDefaultMode || !currentUser ) {
if ( !currentUser ) {
return null;
}
@@ -91,21 +87,21 @@ const TaxonNamesSetting = ( { onChange }: Props ) => {
checked={commonNameFirst}
onPress={() => changeTaxonNameDisplay( NAME_DISPLAY_COM_SCI )}
label={t( "Common-Name-Scientific-Name" )}
classNames="mt-[22px]"
classNames="ml-[6px] mt-[15px]"
/>
<RadioButtonRow
smallLabel
checked={scientificNameFirst}
onPress={() => changeTaxonNameDisplay( NAME_DISPLAY_SCI_COM )}
label={t( "Scientific-Name-Common-Name" )}
classNames="mt-4"
classNames="ml-[6px] mt-[15px]"
/>
<RadioButtonRow
smallLabel
checked={scientificNameOnly}
onPress={() => changeTaxonNameDisplay( NAME_DISPLAY_SCI )}
label={t( "Scientific-Name" )}
classNames="mt-4"
classNames="ml-[6px] mt-[15px]"
/>
</View>
);

View File

@@ -0,0 +1,89 @@
import {
Body1,
Body2,
INatIcon,
List2
} from "components/SharedComponents";
import { Pressable, View } from "components/styledComponents";
import React from "react";
import { GestureResponderEvent } from "react-native";
import { Switch } from "react-native-paper";
import colors from "styles/tailwindColors";
interface Props {
value: boolean;
classNames?: string;
description?: string;
icon?: string;
label?: string;
labelComponent?: React.JSX.Element;
onValueChange: ( newValue: boolean ) => void;
smallLabel?: boolean;
testID?: string;
disabled?: boolean;
}
const SwitchRow = ( {
value,
classNames,
description,
icon,
label,
labelComponent,
onValueChange,
smallLabel = false,
testID,
disabled = false
}: Props ) => {
const handlePress = ( _e: GestureResponderEvent ) => {
if ( !disabled ) {
onValueChange( !value );
}
};
const Label = smallLabel
? Body2
: Body1;
return (
<Pressable
className={classNames}
testID={testID}
accessibilityRole="switch"
accessibilityState={{ checked: value, disabled }}
onPress={handlePress}
disabled={disabled}
>
<View className="flex-row items-center">
<Switch
value={value}
onValueChange={onValueChange}
disabled={disabled}
color={colors.inatGreen}
testID={`${testID || "Toggle"}.switch`}
/>
<View className="ml-3 flex-row w-5/6">
{labelComponent || (
<Label
maxFontSizeMultiplier={1.5}
className="mr-2"
>
{label}
</Label>
)}
{icon && <INatIcon name={icon} size={19} color={colors.inatGreen} />}
</View>
</View>
{description && (
<List2
maxFontSizeMultiplier={1.5}
className="ml-[32px] mt-[3px]"
>
{description}
</List2>
)}
</Pressable>
);
};
export default SwitchRow;

View File

@@ -61,6 +61,7 @@ export { default as TextInputSheet } from "./Sheets/TextInputSheet";
export { default as TextSheet } from "./Sheets/TextSheet";
export { default as WarningSheet } from "./Sheets/WarningSheet";
export { default as SimpleObservationLocation } from "./SimpleObservationLocation";
export { default as SwitchRow } from "./SwitchRow";
export { default as Tabs } from "./Tabs/Tabs";
export { default as TaxonResult } from "./TaxonResult";
export { default as TaxonSearch } from "./TaxonSearch";

View File

@@ -70,7 +70,7 @@ Add-optional-notes = Add optional notes
Adds-your-vote-of-agreement = Adds your vote of agreement
# Hint for a button that adds a vote of disagreement
Adds-your-vote-of-disagreement = Adds your vote of disagreement
Advanced--interface-mode-with-explainer = Advanced (Upload multiple photos and sounds)
ADVANCED-SETTINGS = ADVANCED SETTINGS
Affiliation = Affiliation: { $site }
After-capturing-or-importing-photos-show = After capturing or importing photos, show:
# Label for button that adds an identification of the same taxon as another identification
@@ -84,7 +84,7 @@ Agree-with-ID-description = Would you like to agree with the ID and suggest the
AI-Camera = AI Camera
ALL = ALL
All = All
All-observation-options = All observation options (including iNaturalist AI Camera, Standard Camera, Uploading from Photo Library, and Sound Recorder)
All-observation-options--list = All observation options: iNaturalist AI Camera, Standard Camera, Uploading from Photo Library, and Sound Recorder
All-observations = All observations
All-observations-need-a-date-and-location-to-be-used-for-science = All observations need a date and location to be used for science. Please edit observations if they need more information.
All-organisms = All organisms
@@ -364,7 +364,6 @@ datetime-format-short = M/d/yy h:mm a
datetime-format-short-with-zone = M/d/yy h:mm a zzz
# Month of December
December = December
Default--interface-mode = Default
DELETE = DELETE
Delete-all-observations = Delete all observations
Delete-comment = Delete comment
@@ -625,7 +624,6 @@ iNaturalist-is-supported-by = iNaturalist is supported by an independent, 501(c)
iNaturalist-is-supported-by-our-community = iNaturalist is supported by our amazing community. From everyday naturalists who add observations and identifications, to curators who manage our taxonomy and help with moderation, to the volunteer translators who make iNaturalist more accessible to worldwide audiences, to our community-based donors, we are extraordinarily grateful to all the people in our community who make iNaturalist the platform it is.
iNaturalist-mission-is-to-connect = iNaturalist's mission is to connect people to nature and advance biodiversity science and conservation.
INATURALIST-MISSION-VISION = INATURALIST'S MISSION & VISION
INATURALIST-MODE = INATURALIST MODE
INATURALIST-NETWORK = INATURALIST NETWORK
INATURALIST-SETTINGS = INATURALIST SETTINGS
# Label for the role a user plays on iNaturalist, e.g. "INATURALIST STAFF"
@@ -714,6 +712,8 @@ MAP = MAP
Map-Area = Map Area
# Month of March
March = March
# Radio button option for navigation flows in Settings
Match-Screen = Match Screen
# Identification category
maverick--identification = Maverick
# Month of May
@@ -837,7 +837,6 @@ Obervations-must-be-manually-added = Observations must be manually added to a tr
Obscured = Obscured
Observation = Observation
Observation-Attribution = Observation: © { $userName } · { $restrictions }
OBSERVATION-BUTTON = OBSERVATION BUTTON
Observation-Copyright = Observation Copyright: © { $userName } · { $restrictions }
Observation-has-no-photos-and-no-sounds = This observation has no photos and no sounds.
# Displayed when user views an obscured location on the ObsDetail map screen
@@ -1216,7 +1215,6 @@ SUBMIT-ID-SUGGESTION = SUBMIT ID SUGGESTION
SUGGEST-ID = SUGGEST ID
# Label for element that suggest an identification
Suggest-ID = SUGGEST ID
SUGGESTIONS = SUGGESTIONS
# Identification category
supporting--identification = Supporting
Switches-to-tab = Switches to { $tab } tab.
@@ -1327,6 +1325,8 @@ Using-location = Using location
Verified-IDs-are-used-for-science-and-conservation = Verified IDs are used for science and conservation
# Listing of app and build versions
Version-app-build = Version { $appVersion } ({ $buildVersion })
# Label for toggling app mode between default and advanced settings
View-Advanced-Settings = View Advanced Settings
VIEW-ALL-X-PLACES = VIEW ALL { $count } PLACES
VIEW-ALL-X-PROJECTS = VIEW ALL { $count } PROJECTS
VIEW-ALL-X-TAXA = VIEW ALL { $count } TAXA

View File

@@ -30,7 +30,7 @@
"Add-optional-notes": "Add optional notes",
"Adds-your-vote-of-agreement": "Adds your vote of agreement",
"Adds-your-vote-of-disagreement": "Adds your vote of disagreement",
"Advanced--interface-mode-with-explainer": "Advanced (Upload multiple photos and sounds)",
"ADVANCED-SETTINGS": "ADVANCED SETTINGS",
"Affiliation": "Affiliation: { $site }",
"After-capturing-or-importing-photos-show": "After capturing or importing photos, show:",
"Agree": "Agree",
@@ -40,7 +40,7 @@
"AI-Camera": "AI Camera",
"ALL": "ALL",
"All": "All",
"All-observation-options": "All observation options (including iNaturalist AI Camera, Standard Camera, Uploading from Photo Library, and Sound Recorder)",
"All-observation-options--list": "All observation options: iNaturalist AI Camera, Standard Camera, Uploading from Photo Library, and Sound Recorder",
"All-observations": "All observations",
"All-observations-need-a-date-and-location-to-be-used-for-science": "All observations need a date and location to be used for science. Please edit observations if they need more information.",
"All-organisms": "All organisms",
@@ -193,7 +193,6 @@
"datetime-format-short": "M/d/yy h:mm a",
"datetime-format-short-with-zone": "M/d/yy h:mm a zzz",
"December": "December",
"Default--interface-mode": "Default",
"DELETE": "DELETE",
"Delete-all-observations": "Delete all observations",
"Delete-comment": "Delete comment",
@@ -360,7 +359,6 @@
"iNaturalist-is-supported-by-our-community": "iNaturalist is supported by our amazing community. From everyday naturalists who add observations and identifications, to curators who manage our taxonomy and help with moderation, to the volunteer translators who make iNaturalist more accessible to worldwide audiences, to our community-based donors, we are extraordinarily grateful to all the people in our community who make iNaturalist the platform it is.",
"iNaturalist-mission-is-to-connect": "iNaturalist's mission is to connect people to nature and advance biodiversity science and conservation.",
"INATURALIST-MISSION-VISION": "INATURALIST'S MISSION & VISION",
"INATURALIST-MODE": "INATURALIST MODE",
"INATURALIST-NETWORK": "INATURALIST NETWORK",
"INATURALIST-SETTINGS": "INATURALIST SETTINGS",
"INATURALIST-STAFF": "{ $inaturalist } STAFF",
@@ -420,6 +418,7 @@
"MAP": "MAP",
"Map-Area": "Map Area",
"March": "March",
"Match-Screen": "Match Screen",
"maverick--identification": "Maverick",
"May": "May",
"MEDIA": "MEDIA",
@@ -494,7 +493,6 @@
"Obscured": "Obscured",
"Observation": "Observation",
"Observation-Attribution": "Observation: © { $userName } · { $restrictions }",
"OBSERVATION-BUTTON": "OBSERVATION BUTTON",
"Observation-Copyright": "Observation Copyright: © { $userName } · { $restrictions }",
"Observation-has-no-photos-and-no-sounds": "This observation has no photos and no sounds.",
"Observation-location-obscured-randomized-point": "This observations location is obscured. You are seeing a randomized point within the obscuration polygon.",
@@ -772,7 +770,6 @@
"SUBMIT-ID-SUGGESTION": "SUBMIT ID SUGGESTION",
"SUGGEST-ID": "SUGGEST ID",
"Suggest-ID": "SUGGEST ID",
"SUGGESTIONS": "SUGGESTIONS",
"supporting--identification": "Supporting",
"Switches-to-tab": "Switches to { $tab } tab.",
"Sync-observations": "Sync observations",
@@ -851,6 +848,7 @@
"Using-location": "Using location",
"Verified-IDs-are-used-for-science-and-conservation": "Verified IDs are used for science and conservation",
"Version-app-build": "Version { $appVersion } ({ $buildVersion })",
"View-Advanced-Settings": "View Advanced Settings",
"VIEW-ALL-X-PLACES": "VIEW ALL { $count } PLACES",
"VIEW-ALL-X-PROJECTS": "VIEW ALL { $count } PROJECTS",
"VIEW-ALL-X-TAXA": "VIEW ALL { $count } TAXA",

View File

@@ -70,7 +70,7 @@ Add-optional-notes = Add optional notes
Adds-your-vote-of-agreement = Adds your vote of agreement
# Hint for a button that adds a vote of disagreement
Adds-your-vote-of-disagreement = Adds your vote of disagreement
Advanced--interface-mode-with-explainer = Advanced (Upload multiple photos and sounds)
ADVANCED-SETTINGS = ADVANCED SETTINGS
Affiliation = Affiliation: { $site }
After-capturing-or-importing-photos-show = After capturing or importing photos, show:
# Label for button that adds an identification of the same taxon as another identification
@@ -84,7 +84,7 @@ Agree-with-ID-description = Would you like to agree with the ID and suggest the
AI-Camera = AI Camera
ALL = ALL
All = All
All-observation-options = All observation options (including iNaturalist AI Camera, Standard Camera, Uploading from Photo Library, and Sound Recorder)
All-observation-options--list = All observation options: iNaturalist AI Camera, Standard Camera, Uploading from Photo Library, and Sound Recorder
All-observations = All observations
All-observations-need-a-date-and-location-to-be-used-for-science = All observations need a date and location to be used for science. Please edit observations if they need more information.
All-organisms = All organisms
@@ -364,7 +364,6 @@ datetime-format-short = M/d/yy h:mm a
datetime-format-short-with-zone = M/d/yy h:mm a zzz
# Month of December
December = December
Default--interface-mode = Default
DELETE = DELETE
Delete-all-observations = Delete all observations
Delete-comment = Delete comment
@@ -625,7 +624,6 @@ iNaturalist-is-supported-by = iNaturalist is supported by an independent, 501(c)
iNaturalist-is-supported-by-our-community = iNaturalist is supported by our amazing community. From everyday naturalists who add observations and identifications, to curators who manage our taxonomy and help with moderation, to the volunteer translators who make iNaturalist more accessible to worldwide audiences, to our community-based donors, we are extraordinarily grateful to all the people in our community who make iNaturalist the platform it is.
iNaturalist-mission-is-to-connect = iNaturalist's mission is to connect people to nature and advance biodiversity science and conservation.
INATURALIST-MISSION-VISION = INATURALIST'S MISSION & VISION
INATURALIST-MODE = INATURALIST MODE
INATURALIST-NETWORK = INATURALIST NETWORK
INATURALIST-SETTINGS = INATURALIST SETTINGS
# Label for the role a user plays on iNaturalist, e.g. "INATURALIST STAFF"
@@ -714,6 +712,8 @@ MAP = MAP
Map-Area = Map Area
# Month of March
March = March
# Radio button option for navigation flows in Settings
Match-Screen = Match Screen
# Identification category
maverick--identification = Maverick
# Month of May
@@ -837,7 +837,6 @@ Obervations-must-be-manually-added = Observations must be manually added to a tr
Obscured = Obscured
Observation = Observation
Observation-Attribution = Observation: © { $userName } · { $restrictions }
OBSERVATION-BUTTON = OBSERVATION BUTTON
Observation-Copyright = Observation Copyright: © { $userName } · { $restrictions }
Observation-has-no-photos-and-no-sounds = This observation has no photos and no sounds.
# Displayed when user views an obscured location on the ObsDetail map screen
@@ -1216,7 +1215,6 @@ SUBMIT-ID-SUGGESTION = SUBMIT ID SUGGESTION
SUGGEST-ID = SUGGEST ID
# Label for element that suggest an identification
Suggest-ID = SUGGEST ID
SUGGESTIONS = SUGGESTIONS
# Identification category
supporting--identification = Supporting
Switches-to-tab = Switches to { $tab } tab.
@@ -1327,6 +1325,8 @@ Using-location = Using location
Verified-IDs-are-used-for-science-and-conservation = Verified IDs are used for science and conservation
# Listing of app and build versions
Version-app-build = Version { $appVersion } ({ $buildVersion })
# Label for toggling app mode between default and advanced settings
View-Advanced-Settings = View Advanced Settings
VIEW-ALL-X-PLACES = VIEW ALL { $count } PLACES
VIEW-ALL-X-PROJECTS = VIEW ALL { $count } PROJECTS
VIEW-ALL-X-TAXA = VIEW ALL { $count } TAXA

View File

@@ -5,8 +5,6 @@ const selector = state => ( {
// Vestigial stuff
obsDetailsTab: state.obsDetailsTab,
setObsDetailsTab: state.setObsDetailsTab,
isAllAddObsOptionsMode: state.isAdvancedUser,
setIsAllAddObsOptionsMode: state.setIsAdvancedUser,
loggedInWhileInDefaultMode: state.loggedInWhileInDefaultMode,
setLoggedInWhileInDefaultMode: state.setLoggedInWhileInDefaultMode,
// newer stuff

View File

@@ -3,10 +3,15 @@ export enum OBS_DETAILS_TAB {
DETAILS = "DETAILS"
}
export enum SCREEN_AFTER_PHOTO_EVIDENCE {
SUGGESTIONS = "Suggestions",
OBS_EDIT = "ObsEdit",
MATCH = "Match"
}
const createLayoutSlice = set => ( {
// Vestigial un-namespaced values
isAdvancedUser: false,
setIsAdvancedUser: ( newValue: boolean ) => set( { isAdvancedUser: newValue } ),
// Values that do not need to be persisted
obsDetailsTab: OBS_DETAILS_TAB.ACTIVITY,
setObsDetailsTab: ( newValue: OBS_DETAILS_TAB ) => set( { obsDetailsTab: newValue } ),
@@ -22,14 +27,36 @@ const createLayoutSlice = set => ( {
setIsDefaultMode: ( newValue: boolean ) => set( state => ( {
layout: {
...state.layout,
isDefaultMode: newValue
isDefaultMode: newValue,
// reset to AICamera mode if default mode is toggled on
// and otherwise, advanced mode default is all options
isAllAddObsOptionsMode: newValue !== true,
// reset to Match screen if default mode is toggled on
// and otherwise, advanced mode default is Suggestions
screenAfterPhotoEvidence: newValue === true
? SCREEN_AFTER_PHOTO_EVIDENCE.MATCH
: SCREEN_AFTER_PHOTO_EVIDENCE.SUGGESTIONS
}
} ) ),
isAdvancedSuggestionsMode: true,
setIsSuggestionsFlowMode: ( newValue: boolean ) => set( state => ( {
// leaving isAdvancedSuggestionsMode here for backwards compatibility
// for anyone who already set ObsEdit, but setting the default value
// to null so we can remove it in the future
isAdvancedSuggestionsMode: null,
screenAfterPhotoEvidence: SCREEN_AFTER_PHOTO_EVIDENCE.MATCH,
setScreenAfterPhotoEvidence: ( newScreen: string ) => set( state => ( {
layout: {
...state.layout,
isAdvancedSuggestionsMode: newValue
screenAfterPhotoEvidence: newScreen,
// let's stop using this isAdvancedSuggestionsMode value once users adjust their settings
// so we can remove it in the future
isAdvancedSuggestionsMode: null
}
} ) ),
isAllAddObsOptionsMode: false,
setIsAllAddObsOptionsMode: ( newValue: boolean ) => set( state => ( {
layout: {
...state.layout,
isAllAddObsOptionsMode: newValue
}
} ) ),
// State to control pivot cards and other onboarding material being shown only once

View File

@@ -35,11 +35,6 @@ beforeAll( uniqueRealmBeforeAll );
afterAll( uniqueRealmAfterAll );
// /UNIQUE REALM SETUP
const toggleAdvancedMode = async ( ) => {
const advancedRadioButton = await screen.findByTestId( "advanced-interface-option" );
fireEvent.press( advancedRadioButton );
};
describe( "LanguageSettings", ( ) => {
it( "uses locale preference of the local device", ( ) => {
renderAppWithComponent( <Settings /> );
@@ -68,7 +63,6 @@ describe( "LanguageSettings", ( ) => {
it( "uses locale preference from server", async ( ) => {
renderAppWithComponent( <Settings /> );
await toggleAdvancedMode( );
const sciNameText = await screen.findByText(
i18next.t( "Scientific-Name", { lang: "ru" } )
);
@@ -77,7 +71,6 @@ describe( "LanguageSettings", ( ) => {
it( "allows change to Swedish and requests remote locale change", async ( ) => {
renderAppWithComponent( <Settings /> );
await toggleAdvancedMode( );
const changeLocaleButton = await screen.findByText(
i18next.t( "CHANGE-APP-LANGUAGE", { lang: "ru" } )
);

View File

@@ -138,9 +138,9 @@ const displayItemByText = text => {
beforeEach( ( ) => {
useStore.setState( {
layout: {
isDefaultMode: false
},
isAdvancedUser: true
isDefaultMode: false,
isAllAddObsOptionsMode: true
}
} );
} );
@@ -358,9 +358,9 @@ describe( "MyObservations", ( ) => {
it( "displays observation status in list view in advanced mode", async () => {
useStore.setState( {
layout: {
isDefaultMode: false
},
isAdvancedUser: true
isDefaultMode: false,
isAllAddObsOptionsMode: true
}
} );
const realm = global.mockRealms[__filename];
expect( realm.objects( "Observation" ).length ).toBeGreaterThan( 0 );

View File

@@ -47,9 +47,9 @@ const mockUser = factory( "LocalUser", {
beforeEach( ( ) => {
useStore.setState( {
layout: {
isDefaultMode: false
},
isAdvancedUser: true
isDefaultMode: false,
isAllAddObsOptionsMode: true
}
} );
} );

View File

@@ -87,9 +87,9 @@ beforeEach( ( ) => {
useStore.setState( {
layout: {
isDefaultMode: true,
shownOnce: {}
},
isAdvancedUser: false
shownOnce: {},
isAllAddObsOptionsMode: false
}
} );
} );

View File

@@ -7,6 +7,7 @@ import {
} from "@testing-library/react-native";
import initI18next from "i18n/initI18next";
import inatjs from "inaturalistjs";
import { SCREEN_AFTER_PHOTO_EVIDENCE } from "stores/createLayoutSlice.ts";
import useStore from "stores/useStore";
import factory, { makeResponse } from "tests/factory";
import { renderApp } from "tests/helpers/render";
@@ -83,9 +84,10 @@ const topSuggestion = {
beforeEach( ( ) => {
useStore.setState( {
layout: {
isDefaultMode: false
},
isAdvancedUser: true
isDefaultMode: false,
isAllAddObsOptionsMode: true,
screenAfterPhotoEvidence: SCREEN_AFTER_PHOTO_EVIDENCE.OBS_EDIT
}
} );
inatjs.computervision.score_image.mockResolvedValue( makeResponse( [topSuggestion] ) );
} );

View File

@@ -7,6 +7,7 @@ import {
import initI18next from "i18n/initI18next";
import inatjs from "inaturalistjs";
import * as ImagePicker from "react-native-image-picker";
import { SCREEN_AFTER_PHOTO_EVIDENCE } from "stores/createLayoutSlice.ts";
import useStore from "stores/useStore";
import factory, { makeResponse } from "tests/factory";
import faker from "tests/helpers/faker";
@@ -91,9 +92,9 @@ beforeEach( ( ) => {
useStore.setState( {
layout: {
isDefaultMode: false,
isAdvancedSuggestionsMode: true
},
isAdvancedUser: true
screenAfterPhotoEvidence: SCREEN_AFTER_PHOTO_EVIDENCE.SUGGESTIONS,
isAllAddObsOptionsMode: true
}
} );
inatjs.computervision.score_image.mockResolvedValue( makeResponse( [topSuggestion] ) );
} );

View File

@@ -11,6 +11,7 @@ import {
import * as usePredictions from "components/Camera/AICamera/hooks/usePredictions.ts";
import inatjs from "inaturalistjs";
import * as useLocationPermission from "sharedHooks/useLocationPermission.tsx";
import { SCREEN_AFTER_PHOTO_EVIDENCE } from "stores/createLayoutSlice.ts";
import useStore from "stores/useStore";
import factory, { makeResponse } from "tests/factory";
import { renderAppWithObservations } from "tests/helpers/render";
@@ -207,9 +208,9 @@ const setupAppWithSignedInUser = async hasLocation => {
currentObservation: observations[0],
layout: {
isDefaultMode: false,
isAdvancedSuggestionsMode: true
},
isAdvancedUser: true
screenAfterPhotoEvidence: SCREEN_AFTER_PHOTO_EVIDENCE.OBS_EDIT,
isAllAddObsOptionsMode: true
}
} );
await renderAppWithObservations( observations, __filename );
return { observations };

View File

@@ -85,9 +85,8 @@ beforeEach( async ( ) => {
useStore.setState( {
layout: {
isDefaultMode: false,
isAdvancedSuggestionsMode: false
},
isAdvancedUser: true
isAllAddObsOptionsMode: true
}
} );
inatjs.computervision.score_image.mockResolvedValue( makeResponse( [topSuggestion] ) );
} );
@@ -138,14 +137,6 @@ describe( "AICamera navigation with advanced user layout", ( ) => {
describe( "to Suggestions", ( ) => {
beforeEach( ( ) => {
useStore.setState( {
layout: {
isDefaultMode: false,
isAdvancedSuggestionsMode: true
},
isAdvancedUser: true
} );
const mockWatchPosition = jest.fn( ( success, _error, _options ) => success( {
coords: {
latitude: 56,

View File

@@ -82,7 +82,11 @@ describe( "AddObsButton", ( ) => {
describe( "with advanced user layout", ( ) => {
beforeEach( ( ) => {
useStore.setState( { isAdvancedUser: true } );
useStore.setState( {
layout: {
isAllAddObsOptionsMode: true
}
} );
} );
it( "opens AddObsModal", async ( ) => {
@@ -117,9 +121,9 @@ describe( "with advanced user layout", ( ) => {
beforeEach( ( ) => {
useStore.setState( {
layout: {
isDefaultMode: false,
isAllAddObsOptionsMode: false
},
isAdvancedUser: true
}
} );
} );

View File

@@ -89,9 +89,9 @@ beforeAll( async () => {
beforeEach( ( ) => {
useStore.setState( {
layout: {
isDefaultMode: false
},
isAdvancedUser: true
isDefaultMode: false,
isAllAddObsOptionsMode: true
}
} );
} );

View File

@@ -153,7 +153,11 @@ describe( "MediaViewer navigation", ( ) => {
}
beforeEach( ( ) => {
useStore.setState( { isAdvancedUser: true } );
useStore.setState( {
layout: {
isAllAddObsOptionsMode: true
}
} );
} );
it( "should show a photo when tapped", async ( ) => {

View File

@@ -86,9 +86,9 @@ const actor = userEvent.setup( );
beforeEach( ( ) => {
useStore.setState( {
layout: {
isDefaultMode: false
},
isAdvancedUser: true
isDefaultMode: false,
isAllAddObsOptionsMode: true
}
} );
inatjs.computervision.score_image.mockResolvedValue( makeResponse( [topSuggestion] ) );
} );

View File

@@ -108,9 +108,9 @@ const uploadObsEditObservation = async options => {
beforeEach( ( ) => {
useStore.setState( {
layout: {
isDefaultMode: false
},
isAdvancedUser: true
isDefaultMode: false,
isAllAddObsOptionsMode: true
}
} );
} );

View File

@@ -6,6 +6,7 @@ import {
} from "@testing-library/react-native";
import initI18next from "i18n/initI18next";
import * as rnImagePicker from "react-native-image-picker";
import { SCREEN_AFTER_PHOTO_EVIDENCE } from "stores/createLayoutSlice.ts";
import useStore from "stores/useStore";
import faker from "tests/helpers/faker";
import { renderApp } from "tests/helpers/render";
@@ -73,8 +74,10 @@ describe( "PhotoLibrary navigation", ( ) => {
global.withAnimatedTimeTravelEnabled( );
beforeEach( ( ) => {
useStore.setState( {
isAdvancedUser: true,
layout: { isAdvancedSuggestionsMode: false }
layout: {
screenAfterPhotoEvidence: SCREEN_AFTER_PHOTO_EVIDENCE.OBS_EDIT,
isAllAddObsOptionsMode: true
}
} );
} );
@@ -112,8 +115,10 @@ describe( "PhotoLibrary navigation when suggestions screen is preferred next scr
global.withAnimatedTimeTravelEnabled();
beforeEach( () => {
useStore.setState( {
isAdvancedUser: true,
layout: { isAdvancedSuggestionsMode: true }
layout: {
screenAfterPhotoEvidence: SCREEN_AFTER_PHOTO_EVIDENCE.SUGGESTIONS,
isAllAddObsOptionsMode: true
}
} );
} );
it( "advances to Suggestions when one photo is selected", async () => {

View File

@@ -37,7 +37,11 @@ afterAll( uniqueRealmAfterAll );
beforeAll( async () => {
await initI18next();
useStore.setState( { isAdvancedUser: true } );
useStore.setState( {
layout: {
isAllAddObsOptionsMode: true
}
} );
} );
describe( "SoundRecorder navigation", ( ) => {

View File

@@ -6,6 +6,7 @@ import {
within
} from "@testing-library/react-native";
import initI18next from "i18n/initI18next";
import { SCREEN_AFTER_PHOTO_EVIDENCE } from "stores/createLayoutSlice.ts";
import useStore from "stores/useStore";
import { renderApp } from "tests/helpers/render";
import setupUniqueRealm from "tests/helpers/uniqueRealm";
@@ -38,10 +39,14 @@ afterAll( uniqueRealmAfterAll );
beforeAll( async () => {
await initI18next();
useStore.setState( {
layout: {
isDefaultMode: false,
isAllAddObsOptionsMode: true
}
} );
} );
beforeEach( ( ) => useStore.setState( { isAdvancedUser: true } ) );
const actor = userEvent.setup( );
const navigateToCamera = async ( ) => {
@@ -60,8 +65,11 @@ describe( "StandardCamera navigation with advanced user layout", ( ) => {
global.withAnimatedTimeTravelEnabled( );
beforeEach( () => {
useStore.setState( {
isAdvancedUser: true,
layout: { isAdvancedSuggestionsMode: false }
layout: {
isDefaultMode: false,
isAllAddObsOptionsMode: true,
screenAfterPhotoEvidence: SCREEN_AFTER_PHOTO_EVIDENCE.OBS_EDIT
}
} );
} );
@@ -103,7 +111,11 @@ describe( "StandardCamera navigation with advanced user layout", ( ) => {
describe( "when navigating to Suggestions", ( ) => {
beforeEach( () => {
useStore.setState( {
layout: { isAdvancedSuggestionsMode: true }
layout: {
isDefaultMode: false,
screenAfterPhotoEvidence: SCREEN_AFTER_PHOTO_EVIDENCE.SUGGESTIONS,
isAllAddObsOptionsMode: true
}
} );
} );

View File

@@ -240,15 +240,14 @@ describe( "Suggestions", ( ) => {
// } );
} );
describe( "when reached from Camera directly", ( ) => {
describe( "when reached from AI Camera directly", ( ) => {
beforeEach( async ( ) => {
await signIn( mockUser, { realm: global.mockRealms[__filename] } );
useStore.setState( {
layout: {
isDefaultMode: false,
isAdvancedSuggestionsMode: true
},
isAdvancedUser: true
isAllAddObsOptionsMode: true
}
} );
inatjs.computervision.score_image
.mockResolvedValue( makeResponse( [topSuggestion] ) );

View File

@@ -80,7 +80,11 @@ describe( "CustomTabBar", () => {
describe( "CustomTabBar with advanced user layout", () => {
beforeAll( ( ) => {
useStore.setState( { isAdvancedUser: true } );
useStore.setState( {
layout: {
isAllAddObsOptionsMode: true
}
} );
} );
afterAll( ( ) => {

View File

@@ -32,7 +32,6 @@ describe( "SimpleUploadBannerContainer", () => {
layout: {
isDefaultMode: false
},
isAdvancedUser: true,
numUnuploadedObservations: 1,
uploadStatus: UPLOAD_PENDING,
syncingStatus: MANUAL_SYNC_IN_PROGRESS
@@ -48,7 +47,6 @@ describe( "SimpleUploadBannerContainer", () => {
layout: {
isDefaultMode: false
},
isAdvancedUser: true,
uploadStatus: UPLOAD_PENDING,
syncingStatus: SYNC_PENDING
} );
@@ -68,7 +66,6 @@ describe( "SimpleUploadBannerContainer", () => {
layout: {
isDefaultMode: false
},
isAdvancedUser: true,
initialNumObservationsInQueue: 1,
numUploadsAttempted: 1,
uploadStatus: UPLOAD_IN_PROGRESS,
@@ -86,7 +83,6 @@ describe( "SimpleUploadBannerContainer", () => {
layout: {
isDefaultMode: false
},
isAdvancedUser: true,
numUploadsAttempted,
uploadStatus: UPLOAD_COMPLETE,
syncingStatus: SYNC_PENDING,
@@ -103,7 +99,6 @@ describe( "SimpleUploadBannerContainer", () => {
layout: {
isDefaultMode: false
},
isAdvancedUser: true,
uploadStatus: UPLOAD_PENDING,
syncingStatus: SYNC_PENDING
} );
@@ -123,7 +118,6 @@ describe( "SimpleUploadBannerContainer", () => {
layout: {
isDefaultMode: false
},
isAdvancedUser: true,
uploadStatus: UPLOAD_IN_PROGRESS,
numUploadsAttempted: 2,
syncingStatus: SYNC_PENDING,
@@ -141,7 +135,6 @@ describe( "SimpleUploadBannerContainer", () => {
layout: {
isDefaultMode: false
},
isAdvancedUser: true,
numUploadsAttempted,
uploadStatus: UPLOAD_COMPLETE,
syncingStatus: SYNC_PENDING,
@@ -174,7 +167,6 @@ describe( "SimpleUploadBannerContainer", () => {
layout: {
isDefaultMode: false
},
isAdvancedUser: true,
...deletionStore,
currentDeleteCount: 1,
deleteQueue: [{}],
@@ -192,7 +184,6 @@ describe( "SimpleUploadBannerContainer", () => {
layout: {
isDefaultMode: false
},
isAdvancedUser: true,
...deletionStore,
deleteError,
initialNumDeletionsInQueue: 2
@@ -212,7 +203,6 @@ describe( "SimpleUploadBannerContainer", () => {
layout: {
isDefaultMode: false
},
isAdvancedUser: true,
uploadStatus: UPLOAD_PENDING,
syncingStatus: SYNC_PENDING,
numOfUserObservations: 1
@@ -234,7 +224,6 @@ describe( "SimpleUploadBannerContainer", () => {
layout: {
isDefaultMode: false
},
isAdvancedUser: true,
uploadStatus: UPLOAD_PENDING,
syncingStatus: SYNC_PENDING
} );

View File

@@ -26,10 +26,9 @@ jest.mock( "@react-navigation/native", ( ) => {
const initialStoreState = useStore.getState( );
const toggleAdvancedMode = async ( ) => {
const advancedRadioButton = await screen
.findByText( /Advanced/ );
fireEvent.press( advancedRadioButton );
const toggleAdvancedSwitch = async ( ) => {
const advancedSwitch = await screen.findByTestId( "advanced-interface-switch.switch" );
fireEvent.press( advancedSwitch );
};
beforeAll( async ( ) => {
@@ -52,9 +51,9 @@ beforeEach( ( ) => {
} );
describe( "Settings", ( ) => {
it( "should toggle the green observation button", async ( ) => {
it( "should toggle the green observation button from all options -> AICamera", async ( ) => {
renderComponent( <Settings /> );
await toggleAdvancedMode( );
await toggleAdvancedSwitch( );
const allObsOptions = await screen.findByLabelText( /All observation options/ );
expect( allObsOptions ).toHaveProp( "accessibilityState", expect.objectContaining( {
checked: true
@@ -106,7 +105,6 @@ describe( "Settings", ( ) => {
test( "should change language immediately via language picker via online results", async ( ) => {
renderComponent( <Settings /> );
await toggleAdvancedMode( );
const changeLanguageButton = await screen.findByText( /CHANGE APP LANGUAGE/ );
fireEvent.press( changeLanguageButton );
const picker = await screen.findByTestId( "ReactNativePicker" );