diff --git a/.eslintrc.js b/.eslintrc.js index 6ba50896a..bcea0d951 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -98,6 +98,7 @@ module.exports = { // The following rules are made available via `eslint-plugin-react-hooks` "react-hooks/rules-of-hooks": 2, "react-hooks/exhaustive-deps": 2, + "react-hooks/react-compiler": "error", "react-native/no-inline-styles": "error", diff --git a/babel.config.js b/babel.config.js index bb4b548f5..9a031e609 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,6 +1,7 @@ module.exports = { presets: ["module:@react-native/babel-preset"], plugins: [ + "babel-plugin-react-compiler", // must run first! "react-native-worklets-core/plugin", "transform-inline-environment-variables", "nativewind/babel", diff --git a/package-lock.json b/package-lock.json index 3a76c18d5..6de782cb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -132,6 +132,7 @@ "@types/sanitize-html": "^2.13.0", "@typescript-eslint/eslint-plugin": "^8.32.1", "babel-plugin-module-resolver": "^5.0.0", + "babel-plugin-react-compiler": "^19.1.0-rc.2", "babel-plugin-transform-inline-environment-variables": "^0.4.4", "babel-plugin-transform-remove-console": "^6.9.4", "chalk": "^5.4.1", @@ -145,6 +146,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-module-resolver": "^1.5.0", + "eslint-plugin-react-hooks": "^6.0.0-rc.1", "eslint-plugin-react-native": "^5.0.0", "eslint-plugin-react-native-a11y": "^3.5.1", "eslint-plugin-simple-import-sort": "^12.0.0", @@ -8151,6 +8153,16 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-plugin-react-compiler": { + "version": "19.1.0-rc.2", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.1.0-rc.2.tgz", + "integrity": "sha512-kSNA//p5fMO6ypG8EkEVPIqAjwIXm5tMjfD1XRPL/sRjYSbJ6UsvORfaeolNWnZ9n310aM0xJP7peW26BuCVzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, "node_modules/babel-plugin-syntax-hermes-parser": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.25.1.tgz", @@ -11149,15 +11161,24 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "6.0.0-rc1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-6.0.0-rc1.tgz", + "integrity": "sha512-I4ntWyjqgGemGtOU85FUdVo00h0i0Y5xvQ7a8EVxyzjOZsxXaxvkKBcYoXbP97QDvDjMzY/nGIvfdB/WRLTGxQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "@babel/plugin-transform-private-methods": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.22.4", + "zod-validation-error": "^3.0.3" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "node_modules/eslint-plugin-react-native": { @@ -21738,15 +21759,28 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-validation-error": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.3.tgz", + "integrity": "sha512-OT5Y8lbUadqVZCsnyFaTQ4/O2mys4tj7PqhdbBCp7McPwvIEKfPtdA6QfPeFQK2/Rz5LgwmAXRJTugBNBi0btw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zustand": { "version": "4.5.7", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", diff --git a/package.json b/package.json index 7f1e5927f..c0e307a67 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,7 @@ "@types/sanitize-html": "^2.13.0", "@typescript-eslint/eslint-plugin": "^8.32.1", "babel-plugin-module-resolver": "^5.0.0", + "babel-plugin-react-compiler": "^19.1.0-rc.2", "babel-plugin-transform-inline-environment-variables": "^0.4.4", "babel-plugin-transform-remove-console": "^6.9.4", "chalk": "^5.4.1", @@ -179,6 +180,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-module-resolver": "^1.5.0", + "eslint-plugin-react-hooks": "^6.0.0-rc.1", "eslint-plugin-react-native": "^5.0.0", "eslint-plugin-react-native-a11y": "^3.5.1", "eslint-plugin-simple-import-sort": "^12.0.0", @@ -203,6 +205,9 @@ "typescript": "5.0.4", "yargs": "^17.7.2" }, + "overrides": { + "eslint-plugin-react-hooks": "^6.0.0-rc.1" + }, "engines": { "node": ">=18" }, diff --git a/src/components/Camera/AICamera/FrameProcessorCamera.js b/src/components/Camera/AICamera/FrameProcessorCamera.js index 296c91e04..fb72e5251 100644 --- a/src/components/Camera/AICamera/FrameProcessorCamera.js +++ b/src/components/Camera/AICamera/FrameProcessorCamera.js @@ -8,6 +8,7 @@ import InatVision from "components/Camera/helpers/visionPluginWrapper"; import type { Node } from "react"; import React, { useEffect, + useRef, useState } from "react"; import { Platform } from "react-native"; @@ -53,8 +54,6 @@ const DEFAULT_CONFIDENCE_THRESHOLD = 70; const DEFAULT_NUM_STORED_RESULTS = 5; const DEFAULT_CROP_RATIO = 1.0; -let framesProcessingTime = []; - const FrameProcessorCamera = ( { animatedProps, cameraRef, @@ -84,6 +83,8 @@ const FrameProcessorCamera = ( { const navigation = useNavigation(); + const framesProcessingTime = useRef( [] ); + // When useLocation changes, we need to reset the stored results useEffect( () => { InatVision.resetStoredResults(); @@ -124,11 +125,11 @@ const FrameProcessorCamera = ( { const handleResults = Worklets.createRunOnJS( ( result, timeTaken ) => { setLastTimestamp( result.timestamp ); - framesProcessingTime.push( timeTaken ); - if ( framesProcessingTime.length === 10 ) { - const avgTime = framesProcessingTime.reduce( ( a, b ) => a + b, 0 ) / 10; + framesProcessingTime.current.push( timeTaken ); + if ( framesProcessingTime.current.length === 10 ) { + const avgTime = framesProcessingTime.current.reduce( ( a, b ) => a + b, 0 ) / 10; onLog( { log: `Average frame processing time over 10 frames: ${avgTime}ms` } ); - framesProcessingTime = []; + framesProcessingTime.current = []; } onTaxaDetected( result ); } ); diff --git a/src/components/Camera/StandardCamera/PhotoCarousel.tsx b/src/components/Camera/StandardCamera/PhotoCarousel.tsx index cf6e2d540..6b4a5136c 100644 --- a/src/components/Camera/StandardCamera/PhotoCarousel.tsx +++ b/src/components/Camera/StandardCamera/PhotoCarousel.tsx @@ -9,6 +9,9 @@ import { FlatList } from "react-native"; import Modal from "react-native-modal"; +import { + type SharedValue +} from "react-native-reanimated"; import Animated, { useAnimatedStyle, withTiming @@ -22,9 +25,7 @@ interface Props { isLandscapeMode?: boolean; isLargeScreen?: boolean; isTablet?: boolean; - rotation?: { - value: number - }; + rotation?: SharedValue; photoUris: string[]; onDelete: ( _uri: string ) => void; } @@ -76,12 +77,11 @@ const PhotoCarousel = ( { transform: [ { rotateZ: rotation - ? withTiming( `${-1 * rotation.value}deg` ) + ? withTiming( `${-1 * rotation.get( )}deg` ) : "0" } ] - } ), - [rotation] + } ) ); const renderSkeleton = useCallback( ( ) => ( takingPhoto diff --git a/src/components/Camera/StandardCamera/StandardCamera.js b/src/components/Camera/StandardCamera/StandardCamera.js index 91c9dc7ef..0fe2e884f 100644 --- a/src/components/Camera/StandardCamera/StandardCamera.js +++ b/src/components/Camera/StandardCamera/StandardCamera.js @@ -72,6 +72,8 @@ const StandardCamera = ( { newPhotoUris, setNewPhotoUris }: Props ): Node => { + "use no memo"; + const hasFlash = device?.hasFlash; const { animatedProps, @@ -127,6 +129,9 @@ const StandardCamera = ( { // Reset camera zoom every time we get into a fresh camera view resetZoom( ); prepareCamera(); + // TODO: I am not sure how to make the react compiler happy here, so I disabled it + // for this hook + // eslint-disable-next-line react-hooks/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps }, [] ) ); diff --git a/src/components/Camera/hooks/usePrepareStoreAndNavigate.ts b/src/components/Camera/hooks/usePrepareStoreAndNavigate.ts index f3b4c825d..1e197ba68 100644 --- a/src/components/Camera/hooks/usePrepareStoreAndNavigate.ts +++ b/src/components/Camera/hooks/usePrepareStoreAndNavigate.ts @@ -14,7 +14,7 @@ import useStore from "stores/useStore"; import savePhotosToPhotoLibrary from "../helpers/savePhotosToPhotoLibrary"; -const usePrepareStoreAndNavigate = ( ): Function => { +const usePrepareStoreAndNavigate = ( ) => { const navigation = useNavigation( ); const { params } = useRoute( ); const addEvidence = params?.addEvidence; @@ -128,8 +128,11 @@ const usePrepareStoreAndNavigate = ( ): Function => { ); const updatedCurrentObservation = Observation .appendObsPhotos( obsPhotos, currentObservation ); - observations[currentObservationIndex] = updatedCurrentObservation; - updateObservations( observations ); + + const updatedObservations = [...observations]; + updatedObservations[currentObservationIndex] = updatedCurrentObservation; + updateObservations( updatedObservations ); + await handleSavingToPhotoLibrary( evidenceToAdd, addPhotoPermissionResult, diff --git a/src/components/Camera/hooks/useRotation.ts b/src/components/Camera/hooks/useRotation.ts index 9eb54b48c..3a440f98d 100644 --- a/src/components/Camera/hooks/useRotation.ts +++ b/src/components/Camera/hooks/useRotation.ts @@ -29,18 +29,17 @@ const useRotation = ( ) => { const rotation = useSharedValue( 0 ); useEffect( ( ) => { - rotation.value = rotationValue( deviceOrientation ); + rotation.set( rotationValue( deviceOrientation ) ); }, [deviceOrientation, rotation] ); const rotatableAnimatedStyle = useAnimatedStyle( () => ( { transform: [ { - rotateZ: withTiming( `${-1 * ( rotation?.value || 0 )}deg` ) + rotateZ: withTiming( `${-1 * ( rotation.get( ) || 0 )}deg` ) } ] - } ), - [deviceOrientation] + } ) ); return { diff --git a/src/components/Camera/hooks/useZoom.ts b/src/components/Camera/hooks/useZoom.ts index 5228998c2..4ac24eaab 100644 --- a/src/components/Camera/hooks/useZoom.ts +++ b/src/components/Camera/hooks/useZoom.ts @@ -63,34 +63,28 @@ const useZoom = ( device: CameraDevice ): object => { const newInitialZoom = !device?.isMultiCam ? minZoom : neutralZoom; - zoom.value = newInitialZoom; - startZoom.value = newInitialZoom; - // Shared values don't cause re-renders and including them in dependency arrays - // causes render-time reads, which violates Reanimated's threading model - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [device?.isMultiCam, minZoom, neutralZoom] ); + zoom.set( newInitialZoom ); + startZoom.set( newInitialZoom ); + }, [device?.isMultiCam, minZoom, neutralZoom, zoom, startZoom] ); const handleZoomButtonPress = ( ) => { if ( zoomTextValue === _.last( zoomButtonOptions ) ) { - zoom.value = withSpring( zoomButtonValues[0] ); + zoom.set( withSpring( zoomButtonValues[0] ) ); setZoomTextValue( zoomButtonOptions[0] ); } else { const zoomIndex = _.indexOf( zoomButtonOptions, zoomTextValue ); - zoom.value = withSpring( zoomButtonValues[zoomIndex + 1] ); + zoom.set( withSpring( zoomButtonValues[zoomIndex + 1] ) ); setZoomTextValue( zoomButtonOptions[zoomIndex + 1] ); } }; const onZoomStart = useCallback( ( ) => { // start pinch-to-zoom - startZoom.value = zoom.value; - // Shared values don't cause re-renders and including them in dependency arrays - // causes render-time reads, which violates Reanimated's threading model - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [] ); + startZoom.set( zoom.get() ); + }, [startZoom, zoom] ); const resetZoom = ( ) => { - zoom.value = initialZoom; + zoom.set( initialZoom ); setZoomTextValue( initialZoomTextValue ); }; @@ -115,20 +109,16 @@ const useZoom = ( device: CameraDevice ): object => { const newZoom = interpolate( newValue, [-1, 0, 1], - [minZoom, startZoom.value, maxZoomWithPinch], + [minZoom, startZoom.get( ), maxZoomWithPinch], Extrapolation.CLAMP ); - zoom.value = newZoom; + zoom.set( newZoom ); runOnJS( updateZoomTextValue )( newZoom ); - // Shared values don't cause re-renders and including them in dependency arrays - // causes render-time reads, which violates Reanimated's threading model - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [maxZoomWithPinch, minZoom, updateZoomTextValue] ); + }, [maxZoomWithPinch, minZoom, updateZoomTextValue, startZoom, zoom] ); const animatedProps = useAnimatedProps < CameraProps >( - () => ( { zoom: zoom.value } ), - [] + () => ( { zoom: zoom.get( ) } ) ); const pinchToZoom = useMemo( ( ) => Gesture.Pinch( ) @@ -158,15 +148,15 @@ const useZoom = ( device: CameraDevice ): object => { .averageTouches( true ) .activateAfterLongPress( 1 ) .onBegin( () => { - isPanActive.value = true; - yDiff.value = 0; + yDiff.set( 0 ); + isPanActive.set( true ); onZoomStart( ); } ) .onChange( ev => { - yDiff.value += ev.changeY; + yDiff.set( value => value + ev.changeY ); // Calculate new zoom value from pan (invert because minus pan is up) const newValue = interpolate( - yDiff.value, + yDiff.get( ), [PAN_ZOOM_MIN_DISTANCE, 0, PAN_ZOOM_MAX_DISTANCE], [-1, 0, 1], Extrapolation.CLAMP @@ -174,7 +164,7 @@ const useZoom = ( device: CameraDevice ): object => { onZoomChange( newValue ); } ) .onEnd( () => { - isPanActive.value = false; + isPanActive.set( false ); } ); return { diff --git a/src/components/FullPageWebView/FullPageWebView.tsx b/src/components/FullPageWebView/FullPageWebView.tsx index 3335d6d2c..494718b04 100644 --- a/src/components/FullPageWebView/FullPageWebView.tsx +++ b/src/components/FullPageWebView/FullPageWebView.tsx @@ -150,6 +150,8 @@ export function onShouldStartLoadWithRequest( } const FullPageWebView = ( ) => { + "use no memo"; + const navigation = useNavigation( ); const { params } = useRoute>( ); const [source, setSource] = useState( { uri: params.initialUrl } ); @@ -190,6 +192,9 @@ const FullPageWebView = ( ) => { } ); } ); } + // TODO: I am not sure how to make the react compiler happy here, so I disabled it + // for this hook + // eslint-disable-next-line react-hooks/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps }, [navigation, params.loggedIn, params.title] ) ); diff --git a/src/components/LoginSignUp/AuthenticationService.ts b/src/components/LoginSignUp/AuthenticationService.ts index b59fa6f41..6c235761b 100644 --- a/src/components/LoginSignUp/AuthenticationService.ts +++ b/src/components/LoginSignUp/AuthenticationService.ts @@ -24,7 +24,7 @@ import changeLanguage from "sharedHelpers/changeLanguage.ts"; import { getInstallID } from "sharedHelpers/installData.ts"; import { log, logFilePath, logWithoutRemote } from "sharedHelpers/logger"; import removeAllFilesFromDirectory from "sharedHelpers/removeAllFilesFromDirectory.ts"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { sleep, unlink } from "sharedHelpers/util.ts"; import { isDebugMode } from "sharedHooks/useDebugMode"; import { storage } from "stores/useStore"; diff --git a/src/components/LoginSignUp/SignUpConfirmationForm.tsx b/src/components/LoginSignUp/SignUpConfirmationForm.tsx index 134b5e48d..aa0d39df6 100644 --- a/src/components/LoginSignUp/SignUpConfirmationForm.tsx +++ b/src/components/LoginSignUp/SignUpConfirmationForm.tsx @@ -66,7 +66,8 @@ const SignUpConfirmationForm = ( ) => { const register = async ( ) => { if ( loading ) { return; } setLoading( true ); - user.login = username; + const processedUser = { ...user }; + processedUser.login = username; // If password is less than 6 characters set error and return if ( password.length < 6 ) { setError( t( "Please-make-sure-your-password-is-at-least-6-characters" ) ); @@ -79,14 +80,14 @@ const SignUpConfirmationForm = ( ) => { setLoading( false ); return; } - user.password = password; + processedUser.password = password; // Because checked === true, the following items are considered to be consented too - user.pi_consent = true; - user.data_transfer_consent = true; - user.preferred_observation_license = "CC-BY-NC"; - user.preferred_photo_license = "CC-BY-NC"; - user.preferred_sound_license = "CC-BY-NC"; - const registrationError = await registerUser( user ); + processedUser.pi_consent = true; + processedUser.data_transfer_consent = true; + processedUser.preferred_observation_license = "CC-BY-NC"; + processedUser.preferred_photo_license = "CC-BY-NC"; + processedUser.preferred_sound_license = "CC-BY-NC"; + const registrationError = await registerUser( processedUser ); if ( registrationError ) { // Currently the error is a string coming directly from the server if ( registrationError === "Username has already been taken" ) { @@ -97,7 +98,7 @@ const SignUpConfirmationForm = ( ) => { setLoading( false ); return; } - const success = await authenticateUser( user.login, user.password, realm ); + const success = await authenticateUser( processedUser.login, processedUser.password, realm ); setLoading( false ); if ( !success ) { navigation.navigate( "Login" ); diff --git a/src/components/MyObservations/helpers/syncRemoteDeletedObservations.ts b/src/components/MyObservations/helpers/syncRemoteDeletedObservations.ts index 3c67cd304..b9d5e2722 100644 --- a/src/components/MyObservations/helpers/syncRemoteDeletedObservations.ts +++ b/src/components/MyObservations/helpers/syncRemoteDeletedObservations.ts @@ -3,7 +3,7 @@ import { } from "api/observations"; import { getJWT } from "components/LoginSignUp/AuthenticationService.ts"; import { format } from "date-fns"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { zustandStorage } from "stores/useStore"; const setParamsWithLastSyncTime = ( ) => { diff --git a/src/components/ObsDetails/ActivityTab/ActivityItem.js b/src/components/ObsDetails/ActivityTab/ActivityItem.js index 1741f9ed1..d36a0fa05 100644 --- a/src/components/ObsDetails/ActivityTab/ActivityItem.js +++ b/src/components/ObsDetails/ActivityTab/ActivityItem.js @@ -60,9 +60,9 @@ const ActivityItem = ( { const navToTaxonDetails = ( ) => ( navigation.navigate( { // Ensure button mashing doesn't open multiple TaxonDetails instances - key: `${route.key}-ActivityItem-TaxonDetails-${taxon.id}`, + key: `${route.key}-ActivityItem-TaxonDetails-${taxon?.id}`, name: "TaxonDetails", - params: { id: taxon.id } + params: { id: taxon?.id } } ) ); diff --git a/src/components/ObsDetails/DQAContainer.js b/src/components/ObsDetails/DQAContainer.js index 248ab9b79..76ceeb258 100644 --- a/src/components/ObsDetails/DQAContainer.js +++ b/src/components/ObsDetails/DQAContainer.js @@ -38,7 +38,7 @@ const DQAContainer = ( ): React.Node => { const [hideErrorSheet, setHideErrorSheet] = useState( true ); const [hideOfflineSheet, setHideOfflineSheet] = useState( true ); - const localObservation = useLocalObservation( observationUUID ); + const { localObservation } = useLocalObservation( observationUUID ); const fetchRemoteObservationEnabled = !localObservation || localObservation?.wasSynced(); const { remoteObservation, @@ -57,13 +57,9 @@ const DQAContainer = ( ): React.Node => { const setNotLoading = useCallback( () => { setLoadingMetric( "none" ); - if ( loadingAgree ) { - setLoadingAgree( false ); - } - if ( loadingDisagree ) { - setLoadingDisagree( false ); - } - }, [loadingAgree, loadingDisagree] ); + setLoadingAgree( false ); + setLoadingDisagree( false ); + }, [] ); const setLoading = ( metric, vote ) => { setLoadingMetric( metric ); @@ -96,8 +92,7 @@ const DQAContainer = ( ): React.Node => { if ( !isRefetching ) { setNotLoading(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isRefetching] ); + }, [isRefetching, setNotLoading] ); const faveMutation = useAuthenticatedMutation( ( faveParams, optsWithAuth ) => faveObservation( faveParams, optsWithAuth ), diff --git a/src/components/ObsDetails/ObsDetailsContainer.js b/src/components/ObsDetails/ObsDetailsContainer.js index 9abb2b09c..cfd327372 100644 --- a/src/components/ObsDetails/ObsDetailsContainer.js +++ b/src/components/ObsDetails/ObsDetailsContainer.js @@ -18,7 +18,7 @@ import React, { import { Alert, LogBox } from "react-native"; import Observation from "realmModels/Observation"; import fetchTaxonAndSave from "sharedHelpers/fetchTaxonAndSave"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { useAuthenticatedMutation, useAuthenticatedQuery, @@ -213,7 +213,11 @@ const ObsDetailsContainer = ( ): Node => { } = state; const queryClient = useQueryClient( ); - const localObservation = useLocalObservation( uuid ); + const { + localObservation, + markDeletedLocally, + markViewedLocally + } = useLocalObservation( uuid ); const fetchRemoteObservationEnabled = !!( !remoteObsWasDeleted @@ -228,7 +232,7 @@ const ObsDetailsContainer = ( ): Node => { fetchRemoteObservationError } = useRemoteObservation( uuid, fetchRemoteObservationEnabled ); - useMarkViewedMutation( localObservation, remoteObservation ); + useMarkViewedMutation( localObservation, markViewedLocally, remoteObservation ); // Translates identification-related params to local state useEffect( ( ) => { @@ -264,15 +268,13 @@ const ObsDetailsContainer = ( ): Node => { }, [fetchRemoteObservationError?.status] ); const confirmRemoteObsWasDeleted = useCallback( ( ) => { if ( localObservation ) { - safeRealmWrite( realm, ( ) => { - localObservation._deleted_at = new Date( ); - }, "adding _deleted_at date in ObsDetailsContainer" ); + markDeletedLocally( ); } if ( navigation.canGoBack( ) ) navigation.goBack( ); }, [ localObservation, - navigation, - realm + markDeletedLocally, + navigation ] ); const observation = localObservation || Observation.mapApiToRealm( remoteObservation ); diff --git a/src/components/ObsDetails/hooks/useMarkViewedMutation.js b/src/components/ObsDetails/hooks/useMarkViewedMutation.js index ac0fe5de9..69156fe27 100644 --- a/src/components/ObsDetails/hooks/useMarkViewedMutation.js +++ b/src/components/ObsDetails/hooks/useMarkViewedMutation.js @@ -3,13 +3,11 @@ import { useQueryClient } from "@tanstack/react-query"; import { markObservationUpdatesViewed } from "api/observations"; -import { RealmContext } from "providers/contexts.ts"; import { useEffect, useState } from "react"; import Observation from "realmModels/Observation"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; import { useAuthenticatedMutation, useCurrentUser, @@ -18,14 +16,12 @@ import { import { fetchObservationUpdatesKey } from "sharedHooks/useObservationsUpdates"; import useStore from "stores/useStore"; -const { useRealm } = RealmContext; - const useMarkViewedMutation = ( localObservation: Object, + markViewedLocally: ( ) => void, remoteObservation: Object ) => { const currentUser = useCurrentUser( ); - const realm = useRealm( ); const setObservationMarkedAsViewedAt = useStore( state => state.setObservationMarkedAsViewedAt ); @@ -39,15 +35,6 @@ const useMarkViewedMutation = ( !!currentUser && !!observation ); - const markViewedLocally = async ( ) => { - if ( !localObservation ) { return; } - safeRealmWrite( realm, ( ) => { - // Flags if all comments and identifications have been viewed - localObservation.comments_viewed = true; - localObservation.identifications_viewed = true; - }, "marking viewed locally in ObsDetailsContainer" ); - }; - const markViewedMutation = useAuthenticatedMutation( ( viewedParams, optsWithAuth ) => markObservationUpdatesViewed( viewedParams, optsWithAuth ), { diff --git a/src/components/ObsDetailsDefaultMode/CommunitySection/ActivityItem.js b/src/components/ObsDetailsDefaultMode/CommunitySection/ActivityItem.js index e63bc3c97..b6f8ba253 100644 --- a/src/components/ObsDetailsDefaultMode/CommunitySection/ActivityItem.js +++ b/src/components/ObsDetailsDefaultMode/CommunitySection/ActivityItem.js @@ -63,9 +63,9 @@ const ActivityItem = ( { const navToTaxonDetails = ( ) => ( navigation.navigate( { // Ensure button mashing doesn't open multiple TaxonDetails instances - key: `${route.key}-ActivityItem-TaxonDetails-${taxon.id}`, + key: `${route.key}-ActivityItem-TaxonDetails-${taxon?.id}`, name: "TaxonDetails", - params: { id: taxon.id } + params: { id: taxon?.id } } ) ); diff --git a/src/components/ObsDetailsDefaultMode/ObsDetailsDefaultModeContainer.js b/src/components/ObsDetailsDefaultMode/ObsDetailsDefaultModeContainer.js index 643df56eb..7a994983f 100644 --- a/src/components/ObsDetailsDefaultMode/ObsDetailsDefaultModeContainer.js +++ b/src/components/ObsDetailsDefaultMode/ObsDetailsDefaultModeContainer.js @@ -17,7 +17,7 @@ import React, { } from "react"; import { LogBox } from "react-native"; import Observation from "realmModels/Observation"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { useAuthenticatedQuery, useCurrentUser, @@ -133,7 +133,11 @@ const ObsDetailsDefaultModeContainer = ( ): Node => { } = state; const queryClient = useQueryClient( ); - const localObservation = useLocalObservation( uuid ); + const { + localObservation, + markDeletedLocally, + markViewedLocally + } = useLocalObservation( uuid ); const wasSynced = localObservation && localObservation?.wasSynced(); const fetchRemoteObservationEnabled = !!( @@ -149,7 +153,7 @@ const ObsDetailsDefaultModeContainer = ( ): Node => { fetchRemoteObservationError } = useRemoteObservation( uuid, fetchRemoteObservationEnabled ); - useMarkViewedMutation( localObservation, remoteObservation ); + useMarkViewedMutation( localObservation, markViewedLocally, remoteObservation ); // If we tried to get a remote observation but it no longer exists, the user // can't do anything so we need to send them back and remove the local @@ -159,15 +163,13 @@ const ObsDetailsDefaultModeContainer = ( ): Node => { }, [fetchRemoteObservationError?.status] ); const confirmRemoteObsWasDeleted = useCallback( ( ) => { if ( localObservation ) { - safeRealmWrite( realm, ( ) => { - localObservation._deleted_at = new Date( ); - }, "adding _deleted_at date in ObsDetailsContainer" ); + markDeletedLocally( ); } if ( navigation.canGoBack( ) ) navigation.goBack( ); }, [ localObservation, - navigation, - realm + markDeletedLocally, + navigation ] ); const observation = localObservation || Observation.mapApiToRealm( remoteObservation ); diff --git a/src/components/ObsDetailsDefaultMode/ObsDetailsDefaultModeHeaderRight.tsx b/src/components/ObsDetailsDefaultMode/ObsDetailsDefaultModeHeaderRight.tsx index 1f399eb29..188d2fed0 100644 --- a/src/components/ObsDetailsDefaultMode/ObsDetailsDefaultModeHeaderRight.tsx +++ b/src/components/ObsDetailsDefaultMode/ObsDetailsDefaultModeHeaderRight.tsx @@ -14,11 +14,11 @@ import colors from "styles/tailwindColors"; import HeaderKebabMenu from "./HeaderKebabMenu"; interface Props { - belongsToCurrentUser?: boolean, - observationId: number, - uuid: string, - refetchSubscriptions: Function, - subscriptions: object + belongsToCurrentUser?: boolean; + observationId: number; + uuid: string; + refetchSubscriptions: () => void; + subscriptions: object; } const ObsDetailsDefaultModeHeaderRight = ( { @@ -29,7 +29,7 @@ const ObsDetailsDefaultModeHeaderRight = ( { subscriptions }: Props ) => { const navigation = useNavigation( ); - const localObservation = useLocalObservation( uuid ); + const { localObservation } = useLocalObservation( uuid ); const { t } = useTranslation( ); const navigateToObsEdit = useNavigateToObsEdit( ); diff --git a/src/components/ObsDetailsDefaultMode/hooks/useMarkViewedMutation.js b/src/components/ObsDetailsDefaultMode/hooks/useMarkViewedMutation.js index ac0fe5de9..69156fe27 100644 --- a/src/components/ObsDetailsDefaultMode/hooks/useMarkViewedMutation.js +++ b/src/components/ObsDetailsDefaultMode/hooks/useMarkViewedMutation.js @@ -3,13 +3,11 @@ import { useQueryClient } from "@tanstack/react-query"; import { markObservationUpdatesViewed } from "api/observations"; -import { RealmContext } from "providers/contexts.ts"; import { useEffect, useState } from "react"; import Observation from "realmModels/Observation"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; import { useAuthenticatedMutation, useCurrentUser, @@ -18,14 +16,12 @@ import { import { fetchObservationUpdatesKey } from "sharedHooks/useObservationsUpdates"; import useStore from "stores/useStore"; -const { useRealm } = RealmContext; - const useMarkViewedMutation = ( localObservation: Object, + markViewedLocally: ( ) => void, remoteObservation: Object ) => { const currentUser = useCurrentUser( ); - const realm = useRealm( ); const setObservationMarkedAsViewedAt = useStore( state => state.setObservationMarkedAsViewedAt ); @@ -39,15 +35,6 @@ const useMarkViewedMutation = ( !!currentUser && !!observation ); - const markViewedLocally = async ( ) => { - if ( !localObservation ) { return; } - safeRealmWrite( realm, ( ) => { - // Flags if all comments and identifications have been viewed - localObservation.comments_viewed = true; - localObservation.identifications_viewed = true; - }, "marking viewed locally in ObsDetailsContainer" ); - }; - const markViewedMutation = useAuthenticatedMutation( ( viewedParams, optsWithAuth ) => markObservationUpdatesViewed( viewedParams, optsWithAuth ), { diff --git a/src/components/ObsEdit/Sheets/DeleteObservationSheet.js b/src/components/ObsEdit/Sheets/DeleteObservationSheet.js index 86135a562..2ac3ba523 100644 --- a/src/components/ObsEdit/Sheets/DeleteObservationSheet.js +++ b/src/components/ObsEdit/Sheets/DeleteObservationSheet.js @@ -6,7 +6,7 @@ import { import { RealmContext } from "providers/contexts.ts"; import type { Node } from "react"; import React, { useCallback } from "react"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { useTranslation } from "sharedHooks"; import useStore from "stores/useStore"; diff --git a/src/components/Onboarding/OnboardingCarousel.js b/src/components/Onboarding/OnboardingCarousel.js index d9b0b53d6..9942fe99f 100644 --- a/src/components/Onboarding/OnboardingCarousel.js +++ b/src/components/Onboarding/OnboardingCarousel.js @@ -80,7 +80,7 @@ const OnboardingCarousel = ( ) => { const paginationColor = colors.white; const backgroundAnimation1 = useAnimatedStyle( () => { const opacity = interpolate( - progress.value, + progress.get(), [-1, 0, 1], // Fade in/out around current index [0, 1, 0] // Opacity transitions ); @@ -89,7 +89,7 @@ const OnboardingCarousel = ( ) => { const backgroundAnimation2 = useAnimatedStyle( () => { const opacity = interpolate( - progress.value, + progress.get(), [0, 1, 2], // Fade in/out around current index [0, 1, 0] // Opacity transitions ); @@ -98,7 +98,7 @@ const OnboardingCarousel = ( ) => { const backgroundAnimation3 = useAnimatedStyle( () => { const opacity = interpolate( - progress.value, + progress.get(), [1, 2, 3], // Fade in/out around current index [0, 1, 0] // Opacity transitions ); @@ -251,7 +251,7 @@ const OnboardingCarousel = ( ) => { scrollAnimationDuration={400} onProgressChange={( offsetProgress, absoluteProgress ) => { setCurrentIndex( Math.round( absoluteProgress ) ); - progress.value = absoluteProgress; + progress.set( absoluteProgress ); }} onScrollEnd={() => { setCurrentIndex( carouselRef.current?.getCurrentIndex() ); diff --git a/src/components/PhotoImporter/PhotoLibrary.js b/src/components/PhotoImporter/PhotoLibrary.js index c4d9e5443..b42e970e2 100644 --- a/src/components/PhotoImporter/PhotoLibrary.js +++ b/src/components/PhotoImporter/PhotoLibrary.js @@ -185,8 +185,11 @@ const PhotoLibrary = ( ): Node => { updatedCurrentObservation = Observation .appendObsPhotos( obsPhotos, updatedCurrentObservation ); - observations[currentObservationIndex] = updatedCurrentObservation; - updateObservations( observations ); + + const updatedObservations = [...observations]; + updatedObservations[currentObservationIndex] = updatedCurrentObservation; + updateObservations( updatedObservations ); + navToObsEdit(); setPhotoLibraryShown( false ); } else if ( selectedImages.length === 1 ) { diff --git a/src/components/ProjectDetails/ProjectDetailsContainer.js b/src/components/ProjectDetails/ProjectDetailsContainer.js index d90a7fa4b..24ac30560 100644 --- a/src/components/ProjectDetails/ProjectDetailsContainer.js +++ b/src/components/ProjectDetails/ProjectDetailsContainer.js @@ -9,7 +9,7 @@ import { fetchProjectMembers, fetchProjectPosts, fetchProjects, joinProject, leaveProject } from "api/projects"; import type { Node } from "react"; -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import User from "realmModels/User.ts"; import { log } from "sharedHelpers/logger"; import { useAuthenticatedMutation, useAuthenticatedQuery, useCurrentUser } from "sharedHooks"; @@ -152,19 +152,33 @@ const ProjectDetailsContainer = ( ): Node => { } }; - if ( project ) { - project.members_count = projectMembers?.total_results; - project.journal_posts_count = projectPosts; - project.observations_count = projectStats?.total_results; - project.species_count = speciesCounts?.total_results; - project.current_user_is_member = currentMembership === 1; - project.current_user_observations_count = usersObservations?.total_results; - project.place = projectPlace; - } + const enrichedProject = useMemo( () => { + if ( !project ) return null; + + return { + ...project, + members_count: projectMembers?.total_results, + journal_posts_count: projectPosts, + observations_count: projectStats?.total_results, + species_count: speciesCounts?.total_results, + current_user_is_member: currentMembership === 1, + current_user_observations_count: usersObservations?.total_results, + place: projectPlace + }; + }, [ + project, + projectMembers?.total_results, + projectPosts, + projectStats?.total_results, + speciesCounts?.total_results, + currentMembership, + usersObservations?.total_results, + projectPlace + ] ); return ( { setLoading( true ); diff --git a/src/components/Settings/TaxonNamesSetting.tsx b/src/components/Settings/TaxonNamesSetting.tsx index 19da6dd48..b0d511d08 100644 --- a/src/components/Settings/TaxonNamesSetting.tsx +++ b/src/components/Settings/TaxonNamesSetting.tsx @@ -8,26 +8,23 @@ import React, { useCallback } from "react"; import { View } from "react-native"; -// import { log } from "sharedHelpers/logger"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import User, { TaxonNamesSettings } from "realmModels/User.ts"; import { useCurrentUser, useTranslation } from "sharedHooks"; -// const logger = log.extend( "TaxonNamesSetting" ); - const { useRealm } = RealmContext; const NAME_DISPLAY_COM_SCI = "com-sci"; const NAME_DISPLAY_SCI_COM = "sci-com"; const NAME_DISPLAY_SCI = "sci"; +type NameDisplayPref = + typeof NAME_DISPLAY_COM_SCI | typeof NAME_DISPLAY_SCI_COM | typeof NAME_DISPLAY_SCI; + type Props = { - onChange: ( options: { - prefers_common_names: boolean, - prefers_scientific_name_first: boolean - } ) => void; + onChange: ( options: TaxonNamesSettings ) => void; } const TaxonNamesSetting = ( { onChange }: Props ) => { @@ -35,10 +32,8 @@ const TaxonNamesSetting = ( { onChange }: Props ) => { const { t } = useTranslation( ); const currentUser = useCurrentUser( ); - const changeTaxonNameDisplay = useCallback( nameDisplayPref => { - const options = {}; - - // logger.info( `Changing taxon name display to: ${nameDisplayPref}` ); + const changeTaxonNameDisplay = useCallback( ( nameDisplayPref: NameDisplayPref ) => { + const options: Partial = {}; if ( nameDisplayPref === NAME_DISPLAY_COM_SCI ) { options.prefers_common_names = true; @@ -51,16 +46,7 @@ const TaxonNamesSetting = ( { onChange }: Props ) => { options.prefers_scientific_name_first = false; } - // logger.info( "Writing to realm with options:", options ); - - safeRealmWrite( realm, ( ) => { - currentUser.prefers_common_names = options.prefers_common_names; - currentUser.prefers_scientific_name_first = options.prefers_scientific_name_first; - // logger.info( - // eslint-disable-next-line max-len - // `Realm updated for user ${currentUser.login}, prefers_common_names: ${currentUser.prefers_common_names}, prefers_scientific_name_first: ${currentUser.prefers_scientific_name_first}`; - // ); - }, "saving user in TaxonNamesSetting" ); + User.updatePreferences( realm, options ); onChange( options ); return currentUser; }, [currentUser, realm, onChange] ); diff --git a/src/components/SharedComponents/ActivityAnimation/Confetti.tsx b/src/components/SharedComponents/ActivityAnimation/Confetti.tsx index 606ae3a9a..256b1f34e 100644 --- a/src/components/SharedComponents/ActivityAnimation/Confetti.tsx +++ b/src/components/SharedComponents/ActivityAnimation/Confetti.tsx @@ -14,6 +14,9 @@ import Animated, { useSharedValue, withTiming } from "react-native-reanimated"; +import { + type SharedValue +} from "react-native-reanimated"; import colors from "styles/tailwindColors"; type ConfettiProps = PropsWithChildren<{ @@ -24,7 +27,7 @@ type ConfettiProps = PropsWithChildren<{ type AnimatedElementProps = PropsWithChildren<{ index: number count: number - animation: Animated.SharedValue + animation: SharedValue duration: number }> @@ -46,20 +49,20 @@ const AnimatedElement = memo( transform: [ { translateX: interpolate( - animation.value, + animation.get( ), [startTime, endTime], [start.x * width, end.x * width] ) }, { translateY: interpolate( - animation.value, + animation.get( ), [startTime, endTime], [start.y * height, ( end.y * height ) / 2] ) }, { - rotate: `${interpolate( animation.value, [startTime, endTime], [0, rotation] )}rad` + rotate: `${interpolate( animation.get( ), [startTime, endTime], [0, rotation] )}rad` }, { scale @@ -75,7 +78,7 @@ const Confetti = ( { count, duration = 5000 }: ConfettiProps ) => { const [autoDestroy, setAutoDestroy] = useState( false ); useEffect( () => { - animation.value = withTiming( + animation.set( withTiming( 1, { duration, @@ -86,7 +89,7 @@ const Confetti = ( { count, duration = 5000 }: ConfettiProps ) => { runOnJS( setAutoDestroy )( true ); } } - ); + ) ); }, [ animation, duration @@ -94,7 +97,7 @@ const Confetti = ( { count, duration = 5000 }: ConfettiProps ) => { const fadeOutBeforeDestroy = useSharedValue( 300 / duration ); const stylez = useAnimatedStyle( () => ( { - opacity: interpolate( animation.value, [1 - fadeOutBeforeDestroy.value, 1], [1, 0] ) + opacity: interpolate( animation.get( ), [1 - fadeOutBeforeDestroy.get( ), 1], [1, 0] ) } ) ); if ( autoDestroy ) { diff --git a/src/components/SharedComponents/ActivityAnimation/IndeterminateProgressBar.tsx b/src/components/SharedComponents/ActivityAnimation/IndeterminateProgressBar.tsx index 126678e93..5ba514a57 100644 --- a/src/components/SharedComponents/ActivityAnimation/IndeterminateProgressBar.tsx +++ b/src/components/SharedComponents/ActivityAnimation/IndeterminateProgressBar.tsx @@ -29,7 +29,7 @@ const IndeterminateProgressBar = ( { useEffect( () => { // withRepeat to repeat the animation - translateX.value = withRepeat( + translateX.set( withRepeat( // withDelay to add a delay to our animation withDelay( DURATION / 2, @@ -39,13 +39,13 @@ const IndeterminateProgressBar = ( { ), // Set number of repetitions to -1 to loop indefinitely -1 - ); + ) ); }, [translateX] ); const progress = useAnimatedStyle( () => ( { width: PROGRESS_WIDTH, height: HEIGHT, - transform: [{ translateX: translateX.value }] + transform: [{ translateX: translateX.get( ) }] } ) ); return ( diff --git a/src/components/SharedComponents/Buttons/RotatingINatIconButton.js b/src/components/SharedComponents/Buttons/RotatingINatIconButton.js index 50cef2ca8..61baa6e72 100644 --- a/src/components/SharedComponents/Buttons/RotatingINatIconButton.js +++ b/src/components/SharedComponents/Buttons/RotatingINatIconButton.js @@ -60,11 +60,10 @@ const RotatingINatIconButton = ( { () => ( { transform: [ { - rotateZ: `${rotation.value}deg` + rotateZ: `${rotation.get( )}deg` } ] - } ), - [rotation.value] + } ) ); const getRotationAnimation = toValue => withDelay( @@ -78,20 +77,20 @@ const RotatingINatIconButton = ( { useEffect( () => { const cleanup = () => { cancelAnimation( rotation ); - rotation.value = 0; - // Trigger oen more render to ensure the rotation gets reset to 0 + rotation.set( 0 ); + // Trigger one more render to ensure the rotation gets reset to 0 setNeedsReRender( true ); }; if ( rotating ) { - rotation.value = withRepeat( + rotation.set( withRepeat( withSequence( getRotationAnimation( 180 ), getRotationAnimation( 360 ), withTiming( 0, { duration: 0 } ) ), -1 - ); + ) ); } else { cleanup(); } diff --git a/src/components/SharedComponents/CircleDots.tsx b/src/components/SharedComponents/CircleDots.tsx index f598b4006..510e6bcc3 100644 --- a/src/components/SharedComponents/CircleDots.tsx +++ b/src/components/SharedComponents/CircleDots.tsx @@ -29,30 +29,30 @@ const CircleDots = ( { }: Props ) => { const animation = useSharedValue( 0 ); const rotation = useDerivedValue( ( ) => interpolate( - animation.value, + animation.get( ), [0, 1], [0, 360] ) ); const rotate = useAnimatedStyle( ( ) => ( { transform: [ { - rotateZ: `${rotation.value}deg` + rotateZ: `${rotation.get( )}deg` } ] - } ), [rotation.value] ); + } ) ); const startAnimation = useCallback( ( ) => { - animation.value = withRepeat( + animation.set( withRepeat( withTiming( 1, { duration: 10000, easing: Easing.linear } ), -1 - ); + ) ); }, [animation] ); const stopAnimation = useCallback( ( ) => { - animation.value = 0; + animation.set( 0 ); }, [animation] ); useEffect( ( ) => { diff --git a/src/components/SharedComponents/DisplayTaxonName.tsx b/src/components/SharedComponents/DisplayTaxonName.tsx index f7e8092ba..123924aa8 100644 --- a/src/components/SharedComponents/DisplayTaxonName.tsx +++ b/src/components/SharedComponents/DisplayTaxonName.tsx @@ -89,9 +89,9 @@ const DisplayTaxonName = ( { : taxon; // this is mostly for the AICamera, but might be helpful to display elsewhere - if ( taxonPojo?.rank_level && !taxonPojo?.rank ) { - taxonPojo.rank = rankNames[taxonPojo?.rank_level]; - } + const processedTaxon = taxonPojo?.rank_level && !taxonPojo?.rank + ? { ...taxonPojo, rank: rankNames[taxonPojo.rank_level] } + : taxonPojo; const { commonName, @@ -99,7 +99,7 @@ const DisplayTaxonName = ( { rankPiece, rankLevel, rank - } = generateTaxonPieces( taxonPojo ); + } = generateTaxonPieces( processedTaxon ); const isHorizontal = layout === "horizontal"; const getSpaceChar = ( showSpace: boolean ) => ( showSpace && isHorizontal ? " " diff --git a/src/components/SharedComponents/InlineUser/InlineUserBase.js b/src/components/SharedComponents/InlineUser/InlineUserBase.js index 4e0f28461..681889ac6 100644 --- a/src/components/SharedComponents/InlineUser/InlineUserBase.js +++ b/src/components/SharedComponents/InlineUser/InlineUserBase.js @@ -70,7 +70,7 @@ const InlineUserBase = ( { accessibilityLabel={t( "User", { userHandle } )} accessibilityHint={t( "Navigates-to-user-profile" )} onPress={() => { - navigation.navigate( "UserProfile", { userId: user.id } ); + navigation.navigate( "UserProfile", { userId: user?.id } ); }} > {renderUserIcon()} diff --git a/src/components/SoundRecorder/SoundRecorder.js b/src/components/SoundRecorder/SoundRecorder.js index 1c7cf74cb..6e02a4aa5 100644 --- a/src/components/SoundRecorder/SoundRecorder.js +++ b/src/components/SoundRecorder/SoundRecorder.js @@ -85,8 +85,10 @@ const SoundRecorder = (): Node => { } ); updatedCurrentObservation = Observation .appendObsSounds( [obsSound], updatedCurrentObservation ); - observations[currentObservationIndex] = updatedCurrentObservation; - updateObservations( observations ); + + const updatedObservations = [...observations]; + updatedObservations[currentObservationIndex] = updatedCurrentObservation; + updateObservations( updatedObservations ); } }; diff --git a/src/components/TaxonDetails/EstablishmentMeans.tsx b/src/components/TaxonDetails/EstablishmentMeans.tsx index 9570074a5..cc9c60371 100644 --- a/src/components/TaxonDetails/EstablishmentMeans.tsx +++ b/src/components/TaxonDetails/EstablishmentMeans.tsx @@ -12,7 +12,13 @@ import { useTranslation } from "sharedHooks"; const baseUrl = "https://www.inaturalist.org"; interface Props { - taxon: object; + taxon: { + establishment_means?: { + place?: { + display_name?: string; + }; + }; + }; } const EstablishmentMeans = ( { taxon }: Props ) => { @@ -21,7 +27,7 @@ const EstablishmentMeans = ( { taxon }: Props ) => { const establishmentMeans = taxon?.establishment_means?.establishment_means; const displayEstablishmentMeansText = ( ) => { - const placeName = taxon.establishment_means.place.display_name; + const placeName = taxon?.establishment_means?.place.display_name; if ( establishmentMeans === "native" ) { return t( "Native-to-place", { place: placeName } ); } @@ -67,16 +73,18 @@ const EstablishmentMeans = ( { taxon }: Props ) => { ); }; - return taxon?.establishment_means && ( - - {t( "ESTABLISHMENT-MEANS" )} - - {displayEstablishmentMeansText( )} - {" "} - {displaySourceListText( )} - - - ); + return taxon?.establishment_means + ? ( + + {t( "ESTABLISHMENT-MEANS" )} + + {displayEstablishmentMeansText( )} + {" "} + {displaySourceListText( )} + + + ) + : null; }; export default EstablishmentMeans; diff --git a/src/components/TaxonDetails/TaxonDetails.js b/src/components/TaxonDetails/TaxonDetails.js index 0688e709c..ba45dee4e 100644 --- a/src/components/TaxonDetails/TaxonDetails.js +++ b/src/components/TaxonDetails/TaxonDetails.js @@ -122,7 +122,7 @@ const TaxonDetails = ( ): Node => { const obsUuid = fromObsDetails ? _.find( usableRoutes.slice().reverse(), r => r.name === "ObsDetails" ).params.uuid : null; - const localObservation = useLocalObservation( obsUuid ); + const { localObservation } = useLocalObservation( obsUuid ); const { remoteObservation } = useRemoteObservation( obsUuid, !localObservation && !currentEditingObservation diff --git a/src/components/UserProfile/FollowButtonContainer.js b/src/components/UserProfile/FollowButtonContainer.js index 035904dfa..2b09917be 100644 --- a/src/components/UserProfile/FollowButtonContainer.js +++ b/src/components/UserProfile/FollowButtonContainer.js @@ -59,7 +59,7 @@ const FollowButtonContainer = ( { } ); const updateRelationshipsMutate = ( ) => updateRelationshipsMutation.mutate( { - id: relationship.id, + id: relationship?.id, relationship: { following: true } diff --git a/src/components/hooks/useTaxonCommonNames.ts b/src/components/hooks/useTaxonCommonNames.ts index 929142a52..adfbc808a 100644 --- a/src/components/hooks/useTaxonCommonNames.ts +++ b/src/components/hooks/useTaxonCommonNames.ts @@ -2,7 +2,7 @@ import { fetchSpeciesCounts } from "api/observations"; import { RealmContext } from "providers/contexts.ts"; import { useEffect, useState } from "react"; import Taxon from "realmModels/Taxon"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { useAuthenticatedQuery, useLocationPermission } from "sharedHooks"; import fetchCoarseUserLocation from "../../sharedHelpers/fetchCoarseUserLocation"; diff --git a/src/components/hooks/useWorkQueue.ts b/src/components/hooks/useWorkQueue.ts index c01e8e714..5f187d4f3 100644 --- a/src/components/hooks/useWorkQueue.ts +++ b/src/components/hooks/useWorkQueue.ts @@ -4,7 +4,7 @@ import { RealmContext } from "providers/contexts.ts"; import { useEffect, useState } from "react"; import QueueItem from "realmModels/QueueItem.ts"; import { log } from "sharedHelpers/logger"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { useAuthenticatedMutation } from "sharedHooks"; diff --git a/src/i18n/l10n/en.ftl b/src/i18n/l10n/en.ftl index 49901b5c5..3e5cd2b99 100644 --- a/src/i18n/l10n/en.ftl +++ b/src/i18n/l10n/en.ftl @@ -8,7 +8,7 @@ ### * GroupComments (comments beginning w/ ##) are not allowed because all ### strings in this file will be alphabetized and it's impossible to ### determine where group comments should fit in. -### * Keys should match their content closesly but not exceed 100 chars +### * Keys should match their content closely but not exceed 100 chars ### * Try to annotate all strings with comments to provide context for ### translators, especially for fragments and any situation where the ### meaning is open to interpretation without context @@ -29,7 +29,7 @@ A-global-community-for-nature = A global community for nature ABOUT = ABOUT ABOUT-COLLECTION-PROJECTS = ABOUT COLLECTION PROJECTS ABOUT-INATURALIST = ABOUT INATURALIST -# About the Data Quality Assement +# About the Data Quality Assessment ABOUT-THE-DQA = ABOUT THE DQA About-the-DQA-description = The Quality Grade summarizes the accuracy, precision, completeness, relevance, and appropriateness of an iNaturalist observation as biodiversity data. Some attributes are automatically determined, while others are subject to a vote by iNat users. iNaturalist shares licensed "Research Grade" observations with a number of data partners for use in science and conservation. ABOUT-TRADITIONAL-PROJECTS = ABOUT TRADITIONAL PROJECTS diff --git a/src/realmModels/Observation.js b/src/realmModels/Observation.js index 92de22492..bdece0863 100644 --- a/src/realmModels/Observation.js +++ b/src/realmModels/Observation.js @@ -3,7 +3,7 @@ import { Alert } from "react-native"; import { getNowISO } from "sharedHelpers/dateAndTime.ts"; import { log } from "sharedHelpers/logger"; import { readExifFromMultiplePhotos } from "sharedHelpers/parseExif"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import * as uuid from "uuid"; import Application from "./Application"; diff --git a/src/realmModels/ObservationSound.js b/src/realmModels/ObservationSound.js index 50985248d..a1265b5ea 100644 --- a/src/realmModels/ObservationSound.js +++ b/src/realmModels/ObservationSound.js @@ -1,7 +1,7 @@ import { Realm } from "@realm/react"; import { FileUpload } from "inaturalistjs"; import { Platform } from "react-native"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import * as uuid from "uuid"; import Sound from "./Sound"; diff --git a/src/realmModels/QueueItem.ts b/src/realmModels/QueueItem.ts index 2d130c421..5b4632619 100644 --- a/src/realmModels/QueueItem.ts +++ b/src/realmModels/QueueItem.ts @@ -1,5 +1,5 @@ import { Realm } from "@realm/react"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; const MAX_TRIES = 3; diff --git a/src/realmModels/User.ts b/src/realmModels/User.ts index d06b254a7..3cbd4068b 100644 --- a/src/realmModels/User.ts +++ b/src/realmModels/User.ts @@ -1,8 +1,13 @@ import type { ApiUser } from "api/types"; import Realm, { ObjectSchema } from "realm"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import type { RealmUser } from "./types"; +export interface TaxonNamesSettings { + prefers_common_names: boolean; + prefers_scientific_name_first: boolean; +} class User extends Realm.Object { static FIELDS = { icon_url: true, @@ -32,6 +37,14 @@ class User extends Realm.Object { return realm.objects( "User" ).filtered( "signedIn == true" )[0]; } + static updatePreferences( realm: Realm, newPreferences: TaxonNamesSettings ) { + const currentUser = User.currentUser( realm ); + safeRealmWrite( realm, ( ) => { + currentUser.prefers_common_names = newPreferences.prefers_common_names; + currentUser.prefers_scientific_name_first = newPreferences.prefers_scientific_name_first; + }, "updating user preferences" ); + } + static schema: ObjectSchema = { name: "User", primaryKey: "id", diff --git a/src/sharedHelpers/fetchTaxonAndSave.js b/src/sharedHelpers/fetchTaxonAndSave.js index 7a2f96977..fea906465 100644 --- a/src/sharedHelpers/fetchTaxonAndSave.js +++ b/src/sharedHelpers/fetchTaxonAndSave.js @@ -1,7 +1,7 @@ import { fetchTaxon } from "api/taxa"; import { getJWT } from "components/LoginSignUp/AuthenticationService.ts"; import Taxon from "realmModels/Taxon"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; async function fetchTaxonAndSave( id, realm, params = {}, opts = {} ) { const options = { ...opts }; diff --git a/src/sharedHelpers/safeRealmWrite.js b/src/sharedHelpers/safeRealmWrite.ts similarity index 90% rename from src/sharedHelpers/safeRealmWrite.js rename to src/sharedHelpers/safeRealmWrite.ts index a11fcab13..634ee3a48 100644 --- a/src/sharedHelpers/safeRealmWrite.js +++ b/src/sharedHelpers/safeRealmWrite.ts @@ -1,13 +1,12 @@ -// @flow - // this is based on safeWrite from this github issue, but customized for // realmjs: https://stackoverflow.com/questions/39366182/the-realm-is-already-in-a-write-transaction +import Realm from "realm"; const safeRealmWrite = ( - realm: Object, - action: Function, + realm: Realm, + action: () => void, description: string -): Object => { +) => { if ( realm.isInTransaction ) { return action( ); } diff --git a/src/sharedHooks/index.js b/src/sharedHooks/index.js index 48c7b1b9f..cc5c9988f 100644 --- a/src/sharedHooks/index.js +++ b/src/sharedHooks/index.js @@ -25,7 +25,6 @@ export { default as useObservationUpdatesWhenFocused } from "./useObservationUpd export { default as usePerformance } from "./usePerformance"; export { default as useQuery } from "./useQuery"; export { default as useRemoteObservation } from "./useRemoteObservation"; -export { default as useSafeRoute } from "./useSafeRoute"; export { default as useScrollToOffset } from "./useScrollToOffset"; export { default as useShare } from "./useShare"; export { default as useStoredLayout } from "./useStoredLayout"; diff --git a/src/sharedHooks/useAuthenticatedMutation.js b/src/sharedHooks/useAuthenticatedMutation.js index feebf0a86..13ef0a8aa 100644 --- a/src/sharedHooks/useAuthenticatedMutation.js +++ b/src/sharedHooks/useAuthenticatedMutation.js @@ -3,7 +3,6 @@ import { useMutation } from "@tanstack/react-query"; import handleError from "api/error"; import { getJWT } from "components/LoginSignUp/AuthenticationService.ts"; -import { useSafeRoute } from "sharedHooks"; import { log } from "../../react-native-logs.config"; @@ -14,67 +13,59 @@ const logger = log.extend( "useAuthenticatedMutation" ); const useAuthenticatedMutation = ( mutationFunction: Function, mutationOptions: Object = {} -): Object => { - const route = useSafeRoute( ); - - return useMutation( { - mutationFn: async params => { +): Object => useMutation( { + mutationFn: async params => { // Note, getJWTToken() takes care of fetching a new token if the existing // one is expired. We *could* store the token in state with useState if // fetching from RNSInfo becomes a performance issue - const apiToken = await getJWT( ); - const options = { - api_token: apiToken - }; - return mutationFunction( params, options ); - }, - onError: error => { - if ( error.status === 401 + const apiToken = await getJWT( ); + const options = { + api_token: apiToken + }; + return mutationFunction( params, options ); + }, + onError: error => { + if ( error.status === 401 || ( error.errors && error.errors[0]?.errorCode === "401" ) || JSON.stringify( error ).includes( "JWT is missing or invalid" ) ) { - const errorContext = { - routeName: route?.name, - routeParams: route?.params, - mutationName: mutationOptions.mutationKey || "unknown", - timestamp: new Date().toISOString(), - errorMessage: error.message || "JWT is missing or invalid" - }; + const errorContext = { + mutationName: mutationOptions.mutationKey || "unknown", + timestamp: new Date().toISOString(), + errorMessage: error.message || "JWT is missing or invalid" + }; - logger.error( "401 JWT error in mutation:", JSON.stringify( errorContext ) ); + logger.error( "401 JWT error in mutation:", JSON.stringify( errorContext ) ); - // Try to refresh the token explicitly - // Important: We don't await this to avoid blocking the error handling - getJWT( true ).catch( refreshError => { - logger.error( "Failed to refresh token in mutation after 401:", refreshError ); - } ); + // Try to refresh the token explicitly + // Important: We don't await this to avoid blocking the error handling + getJWT( true ).catch( refreshError => { + logger.error( "Failed to refresh token in mutation after 401:", refreshError ); + } ); - return handleError( error, { - context: errorContext, - throw: mutationOptions.throwOnError !== false - } ); - } + return handleError( error, { + context: errorContext, + throw: mutationOptions.throwOnError !== false + } ); + } - if ( error.status === 429 || ( error.response && error.response.status === 429 ) ) { - const errorContext = { - routeName: route?.name, - routeParams: route?.params, - mutationName: mutationOptions.mutationKey || "unknown", - timestamp: new Date().toISOString() - }; + if ( error.status === 429 || ( error.response && error.response.status === 429 ) ) { + const errorContext = { + mutationName: mutationOptions.mutationKey || "unknown", + timestamp: new Date().toISOString() + }; - logger.error( "429 in mutation:", JSON.stringify( errorContext ) ); + logger.error( "429 in mutation:", JSON.stringify( errorContext ) ); - return handleError( error, { - context: errorContext, - throw: mutationOptions.throwOnError !== false - } ); - } + return handleError( error, { + context: errorContext, + throw: mutationOptions.throwOnError !== false + } ); + } - // Call original handleError for non-429 and non-401 errors - return handleError( error ); - }, - ...mutationOptions - } ); -}; + // Call original handleError for non-429 and non-401 errors + return handleError( error ); + }, + ...mutationOptions +} ); export default useAuthenticatedMutation; diff --git a/src/sharedHooks/useAuthenticatedQuery.js b/src/sharedHooks/useAuthenticatedQuery.js index 5b61ac974..e52d11324 100644 --- a/src/sharedHooks/useAuthenticatedQuery.js +++ b/src/sharedHooks/useAuthenticatedQuery.js @@ -2,7 +2,6 @@ import { useQuery } from "@tanstack/react-query"; import { getJWT, isLoggedIn } from "components/LoginSignUp/AuthenticationService.ts"; import { useEffect, useState } from "react"; import { handleRetryDelay, reactQueryRetry } from "sharedHelpers/logging"; -import { useSafeRoute } from "sharedHooks"; const LOGGED_IN_UNKNOWN = null; @@ -14,7 +13,6 @@ const useAuthenticatedQuery = ( queryOptions = {} ) => { const [userLoggedIn, setUserLoggedIn] = useState( LOGGED_IN_UNKNOWN ); - const route = useSafeRoute( ); useEffect( ( ) => { const checkAuth = async ( ) => { @@ -50,9 +48,7 @@ const useAuthenticatedQuery = ( ...queryOptions, retry: queryOptions.retry !== false ? ( failureCount, error ) => reactQueryRetry( failureCount, error, { - queryKey, - routeName: route?.name, - routeParams: route?.params + queryKey } ) : false, retryDelay: ( failureCount, error ) => handleRetryDelay( failureCount, error ), diff --git a/src/sharedHooks/useIconicTaxa.ts b/src/sharedHooks/useIconicTaxa.ts index 8b97bbef8..6d0e33e63 100644 --- a/src/sharedHooks/useIconicTaxa.ts +++ b/src/sharedHooks/useIconicTaxa.ts @@ -4,7 +4,7 @@ import { RealmContext } from "providers/contexts.ts"; import { useEffect, useState } from "react"; import { UpdateMode } from "realm"; import Taxon from "realmModels/Taxon"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { useAuthenticatedQuery } from "sharedHooks"; const { useRealm } = RealmContext; diff --git a/src/sharedHooks/useLocalObservation.ts b/src/sharedHooks/useLocalObservation.ts index 2cd87ffe2..9708bb2cc 100644 --- a/src/sharedHooks/useLocalObservation.ts +++ b/src/sharedHooks/useLocalObservation.ts @@ -1,6 +1,7 @@ import { RealmContext } from "providers/contexts.ts"; import { Results } from "realm"; import type { RealmComment, RealmIdentification, RealmObservation } from "realmModels/types"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; const { useRealm } = RealmContext; @@ -9,19 +10,55 @@ interface Observation extends RealmObservation { visibleIdentifications?: Results; } -const useLocalObservation = ( uuid: string ): Observation | null => { - const realm = useRealm(); +interface UseLocalObservation { + localObservation: Observation | null; + markDeletedLocally: ( ) => void; + markViewedLocally: ( ) => void; +} + +const useLocalObservation = ( uuid: string ): UseLocalObservation => { + const realm = useRealm( ); if ( !uuid ) { - return null; + return { + localObservation: null, + markDeletedLocally: ( ) => { + throw new Error( "UUID is required to update local observation" ); + }, + markViewedLocally: ( ) => { + throw new Error( "UUID is required to mark local observation as viewed" ); + } + }; } const observation = realm.objectForPrimaryKey( "Observation", uuid ); if ( !observation ) { - return null; + return { + localObservation: null, + markDeletedLocally: ( ) => { + throw new Error( "Trying to update non-existing local observation" ); + }, + markViewedLocally: ( ) => { + throw new Error( "Trying to mark non-existing local observation as viewed" ); + } + }; } + const markDeletedLocally = ( ) => { + safeRealmWrite( realm, ( ) => { + observation._deleted_at = new Date( ); + }, "adding _deleted_at date in ObsDetailsContainer" ); + }; + + const markViewedLocally = ( ) => { + safeRealmWrite( realm, ( ) => { + // Flags if all comments and identifications have been viewed + observation.comments_viewed = true; + observation.identifications_viewed = true; + }, "marking viewed locally in ObsDetailsContainer" ); + }; + Object.defineProperties( observation, { visibleComments: { get() { @@ -39,7 +76,11 @@ const useLocalObservation = ( uuid: string ): Observation | null => { } } ); - return observation; + return { + localObservation: observation, + markDeletedLocally, + markViewedLocally + }; }; export default useLocalObservation; diff --git a/src/sharedHooks/useObservationUpdatesWhenFocused.js b/src/sharedHooks/useObservationUpdatesWhenFocused.js index 7e8bf311b..c3fa475c8 100644 --- a/src/sharedHooks/useObservationUpdatesWhenFocused.js +++ b/src/sharedHooks/useObservationUpdatesWhenFocused.js @@ -3,7 +3,7 @@ import { RealmContext } from "providers/contexts.ts"; import { useCallback, useEffect } from "react"; import { AppState } from "react-native"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; const { useRealm } = RealmContext; diff --git a/src/sharedHooks/useObservationsUpdates.js b/src/sharedHooks/useObservationsUpdates.js index 58f6c564e..9d0c5519b 100644 --- a/src/sharedHooks/useObservationsUpdates.js +++ b/src/sharedHooks/useObservationsUpdates.js @@ -3,7 +3,7 @@ import { fetchObservationUpdates } from "api/observations"; import { RealmContext } from "providers/contexts.ts"; import { useEffect } from "react"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { useAuthenticatedQuery } from "sharedHooks"; const { useRealm } = RealmContext; diff --git a/src/sharedHooks/useQuery.ts b/src/sharedHooks/useQuery.ts index 646e6a3ee..d4ad081f0 100644 --- a/src/sharedHooks/useQuery.ts +++ b/src/sharedHooks/useQuery.ts @@ -1,26 +1,20 @@ +import type { QueryFunction } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query"; import { handleRetryDelay, reactQueryRetry } from "sharedHelpers/logging"; -import { useSafeRoute } from "sharedHooks"; // Should work like React Query's useQuery with our custom reactQueryRetry const useNonAuthenticatedQuery = ( queryKey: Array, - queryFunction: Function, + queryFunction: QueryFunction, queryOptions: object = {} -): object => { - const route = useSafeRoute( ); - - return useQuery( { - queryKey: [...queryKey, queryOptions.allowAnonymousJWT], - queryFn: queryFunction, - retry: ( failureCount, error ) => reactQueryRetry( failureCount, error, { - queryKey, - routeName: route?.name, - routeParams: route?.params - } ), - retryDelay: ( failureCount, error ) => handleRetryDelay( failureCount, error ), - ...queryOptions - } ); -}; +) => useQuery( { + queryKey: [...queryKey, queryOptions.allowAnonymousJWT], + queryFn: queryFunction, + retry: ( failureCount, error ) => reactQueryRetry( failureCount, error, { + queryKey + } ), + retryDelay: ( failureCount, error ) => handleRetryDelay( failureCount, error ), + ...queryOptions +} ); export default useNonAuthenticatedQuery; diff --git a/src/sharedHooks/useSafeRoute.js b/src/sharedHooks/useSafeRoute.js deleted file mode 100644 index 61f93b06f..000000000 --- a/src/sharedHooks/useSafeRoute.js +++ /dev/null @@ -1,28 +0,0 @@ -import { useRoute } from "@react-navigation/native"; - -// import { log } from "../../react-native-logs.config"; - -// const logger = log.extend( "useSafeRoute" ); - -/** - * Safely attempts to get the current route info, without crashing if not - * inside a navigation context - * @returns {Object} Route information or empty object if no route available - */ -const useSafeRoute = () => { - try { - const route = useRoute( ); - if ( route ) { - return { - routeName: route.name, - routeParams: route.params - }; - } - } catch ( _e ) { - // console.log( "Route not available from useSafeRoute" ); - } - - return {}; -}; - -export default useSafeRoute; diff --git a/src/sharedHooks/useSuggestions/useOnlineSuggestions.ts b/src/sharedHooks/useSuggestions/useOnlineSuggestions.ts index 3819b47a2..29db98148 100644 --- a/src/sharedHooks/useSuggestions/useOnlineSuggestions.ts +++ b/src/sharedHooks/useSuggestions/useOnlineSuggestions.ts @@ -9,7 +9,7 @@ import { useCallback, useEffect, useState } from "react"; import Taxon from "realmModels/Taxon"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { useAuthenticatedQuery, useCurrentUser @@ -19,14 +19,14 @@ const SCORE_IMAGE_TIMEOUT = 5_000; const { useRealm } = RealmContext; -type OnlineSuggestionsResponse = { - dataUpdatedAt: Date, - onlineSuggestions: object, - loadingOnlineSuggestions: boolean, - timedOut: boolean, - error: object, - resetTimeout: Function - isRefetching: boolean +interface OnlineSuggestionsResponse { + dataUpdatedAt: Date; + onlineSuggestions: object; + loadingOnlineSuggestions: boolean; + timedOut: boolean; + error: object; + resetTimeout: () => void; + isRefetching: boolean; } const useOnlineSuggestions = ( diff --git a/src/sharedHooks/useTaxon.js b/src/sharedHooks/useTaxon.js index c6d601bf5..c4317a0f1 100644 --- a/src/sharedHooks/useTaxon.js +++ b/src/sharedHooks/useTaxon.js @@ -4,7 +4,7 @@ import { fetchTaxon } from "api/taxa"; import i18n from "i18next"; import { RealmContext } from "providers/contexts.ts"; import Taxon from "realmModels/Taxon"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { useAuthenticatedQuery, useCurrentUser diff --git a/src/sharedHooks/useTaxonSearch.ts b/src/sharedHooks/useTaxonSearch.ts index 7d5bbdd5a..a60a101c3 100644 --- a/src/sharedHooks/useTaxonSearch.ts +++ b/src/sharedHooks/useTaxonSearch.ts @@ -7,7 +7,7 @@ import { import Realm, { UpdateMode } from "realm"; import Taxon from "realmModels/Taxon"; import type { RealmTaxon } from "realmModels/types"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import validateRealmSearch from "sharedHelpers/validateRealmSearch.ts"; import { useAuthenticatedQuery, useIconicTaxa } from "sharedHooks"; diff --git a/src/sharedHooks/useUserMe.js b/src/sharedHooks/useUserMe.js index 440037a4b..109315104 100644 --- a/src/sharedHooks/useUserMe.js +++ b/src/sharedHooks/useUserMe.js @@ -2,7 +2,7 @@ import { fetchUserMe } from "api/users"; import { RealmContext } from "providers/contexts.ts"; import { useCallback, useEffect } from "react"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { useAuthenticatedQuery, useCurrentUser diff --git a/src/uploaders/utils/realmSync.ts b/src/uploaders/utils/realmSync.ts index bcc80b184..d0d39ec78 100644 --- a/src/uploaders/utils/realmSync.ts +++ b/src/uploaders/utils/realmSync.ts @@ -1,4 +1,4 @@ -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; function findRecordInRealm( realm: object, diff --git a/tests/helpers/render.js b/tests/helpers/render.js index b7eb84ded..b932f09a4 100644 --- a/tests/helpers/render.js +++ b/tests/helpers/render.js @@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { render, screen } from "@testing-library/react-native"; +import { render, renderHook, screen } from "@testing-library/react-native"; import App from "components/App"; import INatPaperProvider from "providers/INatPaperProvider"; import React from "react"; @@ -96,43 +96,6 @@ async function renderAppWithObservations( await screen.findByTestId( `MyObservations.obsGridItem.${observations[0].uuid}` ); } -/** - * Render a hook within a component - * - * Port of equivalent in react-testing-library - * (https://github.com/testing-library/react-testing-library/blob/edb6344d578a8c224daf0cd6e2984f36cc6e8d86/src/pure.js#L264C1-L290C2), - * but using our renderComponent - */ -function renderHook( renderCallback, options = {} ) { - const { initialProps, ...renderOptions } = options; - const result = React.createRef( ); - - const TestComponent = ( { renderCallbackProps } ) => { - // eslint-disable-next-line testing-library/render-result-naming-convention - const pendingResult = renderCallback( renderCallbackProps ); - - React.useEffect( ( ) => { - result.current = pendingResult; - } ); - - return null; - }; - - const { rerender: baseRerender, unmount } = renderComponent( - , - null, - renderOptions - ); - - function rerender( rerenderCallbackProps ) { - return baseRerender( - - ); - } - - return { result, rerender, unmount }; -} - function wrapInNavigationContainer( component ) { return ( @@ -149,13 +112,35 @@ function wrapInQueryClientContainer( component ) { ); } +const Wrapper = ( { children } ) => ( + + + + + + + {children} + + + + + + +); + +function renderHookInApp( hookToRender ) { + return renderHook( hookToRender, { + wrapper: Wrapper + } ); +} + export { queryClient, renderApp, renderAppWithComponent, renderAppWithObservations, renderComponent, - renderHook, + renderHookInApp, wrapInNavigationContainer, wrapInQueryClientContainer }; diff --git a/tests/helpers/user.js b/tests/helpers/user.js index 42dabcdf7..27e5f51e9 100644 --- a/tests/helpers/user.js +++ b/tests/helpers/user.js @@ -12,7 +12,7 @@ import inatjs from "inaturalistjs"; import nock from "nock"; import RNSInfo from "react-native-sensitive-info"; import changeLanguage from "sharedHelpers/changeLanguage.ts"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { makeResponse } from "tests/factory"; const TEST_JWT = "test-json-web-token"; diff --git a/tests/integration/DataQualityAssesment/DataQualityAssessment.test.js b/tests/integration/DataQualityAssesment/DataQualityAssessment.test.js index d3305a363..0ae78488a 100644 --- a/tests/integration/DataQualityAssesment/DataQualityAssessment.test.js +++ b/tests/integration/DataQualityAssesment/DataQualityAssessment.test.js @@ -49,7 +49,9 @@ jest.mock( "sharedHooks/useAuthenticatedMutation", () => ( { jest.mock( "sharedHooks/useLocalObservation", () => ( { __esModule: true, - default: jest.fn( ( ) => mockObservation ) + default: jest.fn( ( ) => ( { + localObservation: mockObservation + } ) ) } ) ); useRoute.mockImplementation( ( ) => ( { diff --git a/tests/integration/MyObservations.test.js b/tests/integration/MyObservations.test.js index e977e629d..a99fe01e0 100644 --- a/tests/integration/MyObservations.test.js +++ b/tests/integration/MyObservations.test.js @@ -8,7 +8,7 @@ import i18next from "i18next"; import inatjs from "inaturalistjs"; import { flatten } from "lodash"; import React from "react"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { sleep } from "sharedHelpers/util.ts"; import { zustandStorage } from "stores/useStore"; import factory, { makeResponse } from "tests/factory"; diff --git a/tests/integration/MyObservationsSimple.test.js b/tests/integration/MyObservationsSimple.test.js index bfb18740f..066067721 100644 --- a/tests/integration/MyObservationsSimple.test.js +++ b/tests/integration/MyObservationsSimple.test.js @@ -4,7 +4,7 @@ import { screen, waitFor } from "@testing-library/react-native"; import MyObservationsContainer from "components/MyObservations/MyObservationsContainer.tsx"; import React from "react"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import factory from "tests/factory"; import faker from "tests/helpers/faker"; import { renderAppWithComponent } from "tests/helpers/render"; diff --git a/tests/integration/navigation/broken/MyObservations.test.js b/tests/integration/navigation/broken/MyObservations.test.js index 9d31259dc..cd01912de 100644 --- a/tests/integration/navigation/broken/MyObservations.test.js +++ b/tests/integration/navigation/broken/MyObservations.test.js @@ -3,7 +3,7 @@ import { } from "@testing-library/react-native"; import initI18next from "i18n/initI18next"; import inatjs from "inaturalistjs"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import factory, { makeResponse } from "tests/factory"; import faker from "tests/helpers/faker"; import { renderApp } from "tests/helpers/render"; diff --git a/tests/integration/sharedHooks/useCurrentUser.test.js b/tests/integration/sharedHooks/useCurrentUser.test.js index a64092ccc..4eba06e47 100644 --- a/tests/integration/sharedHooks/useCurrentUser.test.js +++ b/tests/integration/sharedHooks/useCurrentUser.test.js @@ -1,5 +1,5 @@ import { renderHook } from "@testing-library/react-native"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { useCurrentUser } from "sharedHooks"; import factory from "tests/factory"; import setupUniqueRealm from "tests/helpers/uniqueRealm"; diff --git a/tests/integration/sharedHooks/useObservationUpdatesWhenFocused.test.js b/tests/integration/sharedHooks/useObservationUpdatesWhenFocused.test.js index fa2a51361..cf462eef0 100644 --- a/tests/integration/sharedHooks/useObservationUpdatesWhenFocused.test.js +++ b/tests/integration/sharedHooks/useObservationUpdatesWhenFocused.test.js @@ -1,5 +1,5 @@ import { renderHook } from "@testing-library/react-native"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import useObservationUpdatesWhenFocused from "sharedHooks/useObservationUpdatesWhenFocused"; import factory from "tests/factory"; import setupUniqueRealm from "tests/helpers/uniqueRealm"; diff --git a/tests/integration/sharedHooks/useObservationsUpdates.test.js b/tests/integration/sharedHooks/useObservationsUpdates.test.js index b60445a75..a36f2c21e 100644 --- a/tests/integration/sharedHooks/useObservationsUpdates.test.js +++ b/tests/integration/sharedHooks/useObservationsUpdates.test.js @@ -1,5 +1,5 @@ import { renderHook } from "@testing-library/react-native"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import useObservationsUpdates from "sharedHooks/useObservationsUpdates"; import factory from "tests/factory"; import faker from "tests/helpers/faker"; diff --git a/tests/integration/sharedHooks/useTaxon.test.js b/tests/integration/sharedHooks/useTaxon.test.js index f4629f851..94a6eefe0 100644 --- a/tests/integration/sharedHooks/useTaxon.test.js +++ b/tests/integration/sharedHooks/useTaxon.test.js @@ -1,10 +1,10 @@ import { waitFor } from "@testing-library/react-native"; import inatjs from "inaturalistjs"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { useTaxon } from "sharedHooks"; import factory, { makeResponse } from "tests/factory"; import faker from "tests/helpers/faker"; -import { renderHook } from "tests/helpers/render"; +import { renderHookInApp } from "tests/helpers/render"; import setupUniqueRealm from "tests/helpers/uniqueRealm"; // UNIQUE REALM SETUP @@ -57,13 +57,13 @@ describe( "with local taxon", ( ) => { } ); it( "should return an object", ( ) => { - const { result } = renderHook( ( ) => useTaxon( mockTaxon ) ); + const { result } = renderHookInApp( ( ) => useTaxon( mockTaxon ) ); expect( result.current ).toBeInstanceOf( Object ); } ); describe( "when there is a local taxon with taxon id", ( ) => { it( "should return local taxon with default photo", ( ) => { - const { result } = renderHook( ( ) => useTaxon( mockTaxon ) ); + const { result } = renderHookInApp( ( ) => useTaxon( mockTaxon ) ); const { taxon: resultTaxon } = result.current; expect( resultTaxon ).toHaveProperty( "default_photo" ); expect( resultTaxon.default_photo.url ).toEqual( mockTaxon.default_photo.url ); @@ -74,7 +74,7 @@ describe( "with local taxon", ( ) => { safeRealmWrite( global.mockRealms[mockRealmIdentifier], ( ) => { global.mockRealms[mockRealmIdentifier].create( "Taxon", mockOutdatedTaxon, "modified" ); }, "write mock outdated taxon, useTaxon test" ); - renderHook( ( ) => useTaxon( mockOutdatedTaxon ) ); + renderHookInApp( ( ) => useTaxon( mockOutdatedTaxon ) ); await waitFor( ( ) => expect( inatjs.taxa.fetch ).toHaveBeenCalled( ) ); } ); } ); @@ -92,7 +92,7 @@ describe( "when there is no local taxon with taxon id", ( ) => { expect( global.mockRealms[mockRealmIdentifier].objectForPrimaryKey( "Taxon", mockTaxon.id ) ).toBeNull( ); - renderHook( ( ) => useTaxon( mockTaxon ) ); + renderHookInApp( ( ) => useTaxon( mockTaxon ) ); await waitFor( ( ) => expect( inatjs.taxa.fetch ).toHaveBeenCalled( ) ); } ); @@ -108,13 +108,13 @@ describe( "when there is no local taxon with taxon id", ( ) => { } ) ) } ) ); const partialTaxon = { id: faker.number.int( ), foo: "bar" }; - const { result } = renderHook( ( ) => useTaxon( partialTaxon ) ); + const { result } = renderHookInApp( ( ) => useTaxon( partialTaxon ) ); expect( result.current.taxon.foo ).toEqual( "bar" ); jest.unmock( "@tanstack/react-query" ); } ); it( "should return a taxon like a local taxon record if the request succeeds", async ( ) => { - const { result } = renderHook( ( ) => useTaxon( { id: mockTaxon.id } ) ); + const { result } = renderHookInApp( ( ) => useTaxon( { id: mockTaxon.id } ) ); await waitFor( ( ) => expect( result.current.taxon ).toHaveProperty( "default_photo" ) ); } ); } ); @@ -125,7 +125,7 @@ describe( "when there is no local taxon with taxon id", ( ) => { expect( global.mockRealms[mockRealmIdentifier].objectForPrimaryKey( "Taxon", taxonId ) ).toBeNull( ); - const { result } = renderHook( ( ) => useTaxon( { id: taxonId, foo: "bar" }, false ) ); + const { result } = renderHookInApp( ( ) => useTaxon( { id: taxonId, foo: "bar" }, false ) ); expect( result.current.taxon ).not.toHaveProperty( "default_photo" ); expect( result.current.taxon.foo ).toEqual( "bar" ); } ); diff --git a/tests/performance/MyObservations.perf-test.js b/tests/performance/MyObservations.perf-test.js index 421e4d9ee..2ef7fe9ad 100644 --- a/tests/performance/MyObservations.perf-test.js +++ b/tests/performance/MyObservations.perf-test.js @@ -5,7 +5,7 @@ import { fireEvent, screen, waitForElementToBeRemoved } from "@testing-library/r import MyObservationsContainer from "components/MyObservations/MyObservationsContainer.tsx"; import React from "react"; import { measureRenders } from "reassure"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import factory from "tests/factory"; import { queryClient } from "tests/helpers/render"; import setupUniqueRealm from "tests/helpers/uniqueRealm"; diff --git a/tests/unit/components/DisplayTaxonName.test.js b/tests/unit/components/DisplayTaxonName.test.js index 9c12ce97f..71a79acc5 100644 --- a/tests/unit/components/DisplayTaxonName.test.js +++ b/tests/unit/components/DisplayTaxonName.test.js @@ -1,7 +1,7 @@ import { render, screen } from "@testing-library/react-native"; import { DisplayTaxonName } from "components/SharedComponents"; import React from "react"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import factory from "tests/factory"; import faker from "tests/helpers/faker"; diff --git a/tests/unit/components/MyObservations/useSyncObservations.test.js b/tests/unit/components/MyObservations/useSyncObservations.test.js index 30c346a33..27ea22533 100644 --- a/tests/unit/components/MyObservations/useSyncObservations.test.js +++ b/tests/unit/components/MyObservations/useSyncObservations.test.js @@ -1,7 +1,7 @@ import { renderHook, waitFor } from "@testing-library/react-native"; import useSyncObservations from "components/MyObservations/hooks/useSyncObservations.ts"; import inatjs from "inaturalistjs"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { BEGIN_AUTOMATIC_SYNC } from "stores/createSyncObservationsSlice.ts"; diff --git a/tests/unit/components/ObsDetails/DataQualityAssessment.test.js b/tests/unit/components/ObsDetails/DataQualityAssessment.test.js index 9b6368d86..49d1b2ded 100644 --- a/tests/unit/components/ObsDetails/DataQualityAssessment.test.js +++ b/tests/unit/components/ObsDetails/DataQualityAssessment.test.js @@ -50,7 +50,9 @@ jest.mock( "sharedHooks/useAuthenticatedMutation", () => ( { jest.mock( "sharedHooks/useLocalObservation", () => ( { __esModule: true, - default: jest.fn( ( ) => mockObservation ) + default: jest.fn( () => ( { + localObservation: mockObservation + } ) ) } ) ); jest.mock( "sharedHooks/useRemoteObservation", ( ) => ( { diff --git a/tests/unit/components/ObsDetails/ObsDetails.test.js b/tests/unit/components/ObsDetails/ObsDetails.test.js index 8b3093a5a..2d73ace63 100644 --- a/tests/unit/components/ObsDetails/ObsDetails.test.js +++ b/tests/unit/components/ObsDetails/ObsDetails.test.js @@ -79,7 +79,9 @@ const mockUser = factory( "LocalUser", { jest.mock( "sharedHooks/useLocalObservation", () => ( { __esModule: true, - default: ( ) => null + default: jest.fn( () => ( { + localObservation: null + } ) ) } ) ); jest.mock( "sharedHooks/useCurrentUser", () => ( { @@ -273,7 +275,9 @@ describe( "ObsDetails", () => { firstIdentification ]; - jest.spyOn( useLocalObservation, "default" ).mockImplementation( () => null ); + jest.spyOn( useLocalObservation, "default" ).mockImplementation( () => ( { + localObservation: null + } ) ); useAuthenticatedQuery.mockReturnValue( { data: otherUserObservation diff --git a/tests/unit/components/ObsDetailsDefaultMode/DataQualityAssessment.test.js b/tests/unit/components/ObsDetailsDefaultMode/DataQualityAssessment.test.js index 9b6368d86..49d1b2ded 100644 --- a/tests/unit/components/ObsDetailsDefaultMode/DataQualityAssessment.test.js +++ b/tests/unit/components/ObsDetailsDefaultMode/DataQualityAssessment.test.js @@ -50,7 +50,9 @@ jest.mock( "sharedHooks/useAuthenticatedMutation", () => ( { jest.mock( "sharedHooks/useLocalObservation", () => ( { __esModule: true, - default: jest.fn( ( ) => mockObservation ) + default: jest.fn( () => ( { + localObservation: mockObservation + } ) ) } ) ); jest.mock( "sharedHooks/useRemoteObservation", ( ) => ( { diff --git a/tests/unit/components/ObsDetailsDefaultMode/ObsDetails.test.js b/tests/unit/components/ObsDetailsDefaultMode/ObsDetails.test.js index ca150a8cf..637818f27 100644 --- a/tests/unit/components/ObsDetailsDefaultMode/ObsDetails.test.js +++ b/tests/unit/components/ObsDetailsDefaultMode/ObsDetails.test.js @@ -76,7 +76,9 @@ const mockUser = factory( "LocalUser", { jest.mock( "sharedHooks/useLocalObservation", () => ( { __esModule: true, - default: ( ) => null + default: jest.fn( () => ( { + localObservation: null + } ) ) } ) ); jest.mock( "sharedHooks/useCurrentUser", () => ( { @@ -183,7 +185,9 @@ describe( "ObsDetails", () => { firstIdentification ]; - jest.spyOn( useLocalObservation, "default" ).mockImplementation( () => null ); + jest.spyOn( useLocalObservation, "default" ).mockImplementation( () => ( { + localObservation: null + } ) ); useAuthenticatedQuery.mockReturnValue( { data: otherUserObservation diff --git a/tests/unit/components/ObsEdit/DeleteObservationSheet.test.js b/tests/unit/components/ObsEdit/DeleteObservationSheet.test.js index 009d63e82..6b08119ac 100644 --- a/tests/unit/components/ObsEdit/DeleteObservationSheet.test.js +++ b/tests/unit/components/ObsEdit/DeleteObservationSheet.test.js @@ -3,7 +3,7 @@ import DeleteObservationSheet from "components/ObsEdit/Sheets/DeleteObservationS import i18next from "i18next"; import inatjs from "inaturalistjs"; import React from "react"; -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import factory from "tests/factory"; import { renderComponent } from "tests/helpers/render"; diff --git a/tests/unit/components/Projects/Projects.test.js b/tests/unit/components/Projects/Projects.test.js index 333a8c452..4152e8734 100644 --- a/tests/unit/components/Projects/Projects.test.js +++ b/tests/unit/components/Projects/Projects.test.js @@ -53,25 +53,6 @@ jest.mock( "@react-navigation/native", ( ) => { }; } ); -// react-native-paper's TextInput does a bunch of async stuff that's hard to -// control in a test, so we're just mocking it here. -jest.mock( "react-native-paper", () => { - const RealModule = jest.requireActual( "react-native-paper" ); - const MockTextInput = props => { - const MockName = "mock-text-input"; - // eslint-disable-next-line react/jsx-props-no-spreading - return {props.children}; - }; - MockTextInput.Icon = RealModule.TextInput.Icon; - const MockedModule = { - ...RealModule, - // eslint-disable-next-line react/jsx-props-no-spreading - // TextInput: props => {props.children} - TextInput: MockTextInput - }; - return MockedModule; -} ); - describe( "Projects", ( ) => { beforeAll( async ( ) => { jest.useFakeTimers( ); diff --git a/tests/unit/components/Settings/TaxonNamesSetting.test.js b/tests/unit/components/Settings/TaxonNamesSetting.test.js index c8c2c88a4..e970583e1 100644 --- a/tests/unit/components/Settings/TaxonNamesSetting.test.js +++ b/tests/unit/components/Settings/TaxonNamesSetting.test.js @@ -25,7 +25,15 @@ jest.mock( "sharedHooks", () => ( { useCurrentUser: () => mockUser } ) ); -jest.mock( "sharedHelpers/safeRealmWrite", () => jest.fn( ( _, callback ) => callback() ) ); +jest.mock( "realmModels/User.ts", () => ( { + __esModule: true, + default: { + updatePreferences: jest.fn( ( _, options ) => { + mockUser.prefers_common_names = options.prefers_common_names; + mockUser.prefers_scientific_name_first = options.prefers_scientific_name_first; + } ) + } +} ) ); describe( "TaxonNamesSetting", () => { test( "toggles between the three name display options correctly", () => { diff --git a/tests/unit/components/SharedComponents/SearchBar.test.js b/tests/unit/components/SharedComponents/SearchBar.test.js index 73528d5d5..fd61ab3ed 100644 --- a/tests/unit/components/SharedComponents/SearchBar.test.js +++ b/tests/unit/components/SharedComponents/SearchBar.test.js @@ -1,25 +1,6 @@ import { SearchBar } from "components/SharedComponents"; import React from "react"; -// react-native-paper's TextInput does a bunch of async stuff that's hard to -// control in a test, so we're just mocking it here. -jest.mock( "react-native-paper", () => { - const RealModule = jest.requireActual( "react-native-paper" ); - const MockTextInput = props => { - const MockName = "mock-text-input"; - // eslint-disable-next-line react/jsx-props-no-spreading - return {props.children}; - }; - MockTextInput.Icon = RealModule.TextInput.Icon; - const MockedModule = { - ...RealModule, - // eslint-disable-next-line react/jsx-props-no-spreading - // TextInput: props => {props.children} - TextInput: MockTextInput - }; - return MockedModule; -} ); - describe( "SearchBar", () => { it( "should be accessible", () => { const searchBar = ( diff --git a/tests/unit/components/Suggestions/SuggestionsTaxonSearch.test.js b/tests/unit/components/Suggestions/SuggestionsTaxonSearch.test.js index 8b6312f23..517a42457 100644 --- a/tests/unit/components/Suggestions/SuggestionsTaxonSearch.test.js +++ b/tests/unit/components/Suggestions/SuggestionsTaxonSearch.test.js @@ -47,25 +47,6 @@ jest.mock( "sharedHooks/useTaxon", () => ( { default: () => ( { taxon: mockTaxaList[0] } ) } ) ); -// react-native-paper's TextInput does a bunch of async stuff that's hard to -// control in a test, so we're just mocking it here. -jest.mock( "react-native-paper", () => { - const RealModule = jest.requireActual( "react-native-paper" ); - const MockTextInput = props => { - const MockName = "mock-text-input"; - // eslint-disable-next-line react/jsx-props-no-spreading - return {props.children}; - }; - MockTextInput.Icon = RealModule.TextInput.Icon; - const MockedModule = { - ...RealModule, - // eslint-disable-next-line react/jsx-props-no-spreading - // TextInput: props => {props.children} - TextInput: MockTextInput - }; - return MockedModule; -} ); - describe( "TaxonSearch", ( ) => { test( "should not have accessibility errors", async ( ) => { // const taxonSearch = ( diff --git a/tests/unit/uploaders/utils/realmSync.test.js b/tests/unit/uploaders/utils/realmSync.test.js index dd1e80694..55d867771 100644 --- a/tests/unit/uploaders/utils/realmSync.test.js +++ b/tests/unit/uploaders/utils/realmSync.test.js @@ -1,4 +1,4 @@ -import safeRealmWrite from "sharedHelpers/safeRealmWrite"; +import safeRealmWrite from "sharedHelpers/safeRealmWrite.ts"; import { markRecordUploaded } from "uploaders"; jest.mock( "sharedHelpers/safeRealmWrite" );