From 2b1bcd93637d21ba85beece441dff8f27925871e Mon Sep 17 00:00:00 2001 From: Amanda Bullington <35536439+albullington@users.noreply.github.com> Date: Tue, 23 Apr 2024 14:26:57 -0700 Subject: [PATCH] Enable multi-camera (#1461) * Use multi-cam; closes #1458 * Typescript conversion --- ios/Podfile.lock | 6 +-- .../project.pbxproj | 12 +---- package-lock.json | 10 ++++ package.json | 1 + src/components/Camera/AICamera/AICamera.js | 4 +- .../Camera/AICamera/FrameProcessorCamera.js | 2 +- ...CameraContainer.js => CameraContainer.tsx} | 17 +++---- .../Camera/{CameraView.js => CameraView.tsx} | 48 ++++++++++--------- ...meraWithDevice.js => CameraWithDevice.tsx} | 9 ++-- .../Camera/StandardCamera/StandardCamera.js | 6 +-- .../{useTakePhoto.js => useTakePhoto.ts} | 7 ++- .../Camera/hooks/{useZoom.js => useZoom.ts} | 5 +- .../NoBottomTabStackNavigator.js | 2 +- tests/unit/components/Camera/AICamera.test.js | 2 +- .../components/Camera/CameraContainer.test.js | 4 +- 15 files changed, 70 insertions(+), 65 deletions(-) rename src/components/Camera/{CameraContainer.js => CameraContainer.tsx} (65%) rename src/components/Camera/{CameraView.js => CameraView.tsx} (94%) rename src/components/Camera/{CameraWithDevice.js => CameraWithDevice.tsx} (97%) rename src/components/Camera/hooks/{useTakePhoto.js => useTakePhoto.ts} (91%) rename src/components/Camera/hooks/{useZoom.js => useZoom.ts} (94%) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index dc394b0bc..f81891387 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1472,12 +1472,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: d3f49c53809116a5d38da093a8aa78bf551aed09 BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3 - DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 + DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 FasterImage: 60d0750ddbcefff0070c4c17309c2d1d6cc650f0 FBLazyVector: 9f533d5a4c75ca77c8ed774aced1a91a0701781e FBReactNativeSpec: 40b791f4a1df779e7e4aa12c000319f4f216d40a fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b + glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 hermes-engine: 39589e9c297d024e90fe68f6830ff86c4e01498a libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 MMKV: ed58ad794b3f88c24d604a5b74f3fba17fcbaf74 @@ -1568,4 +1568,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: eebd76aa39f99b44754431ed68ce0cfbfc5ec2f7 -COCOAPODS: 1.14.2 +COCOAPODS: 1.14.3 diff --git a/ios/iNaturalistReactNative.xcodeproj/project.pbxproj b/ios/iNaturalistReactNative.xcodeproj/project.pbxproj index 07d3423b7..21d038458 100644 --- a/ios/iNaturalistReactNative.xcodeproj/project.pbxproj +++ b/ios/iNaturalistReactNative.xcodeproj/project.pbxproj @@ -941,11 +941,7 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; @@ -1017,11 +1013,7 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; diff --git a/package-lock.json b/package-lock.json index f21666acc..a174523c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@react-native-clipboard/clipboard": "^1.13.2", "@react-native-community/datetimepicker": "^7.6.3", "@react-native-community/geolocation": "^3.2.1", + "@react-native-community/hooks": "^3.0.0", "@react-native-community/netinfo": "^11.3.1", "@react-native-community/slider": "^4.5.0", "@react-native-picker/picker": "^2.7.2", @@ -4811,6 +4812,15 @@ "react-native": "*" } }, + "node_modules/@react-native-community/hooks": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/hooks/-/hooks-3.0.0.tgz", + "integrity": "sha512-g2OyxXHfwIytXUJitBR6Z/ISoOfp0WKx5FOv+NqJ/CrWjRDcTw6zXE5I1C9axfuh30kJqzWchVfCDrkzZYTxqg==", + "peerDependencies": { + "react": ">=17.0.2", + "react-native": ">=0.65" + } + }, "node_modules/@react-native-community/netinfo": { "version": "11.3.1", "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.3.1.tgz", diff --git a/package.json b/package.json index 6b8380f74..311d2b43f 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@react-native-clipboard/clipboard": "^1.13.2", "@react-native-community/datetimepicker": "^7.6.3", "@react-native-community/geolocation": "^3.2.1", + "@react-native-community/hooks": "^3.0.0", "@react-native-community/netinfo": "^11.3.1", "@react-native-community/slider": "^4.5.0", "@react-native-picker/picker": "^2.7.2", diff --git a/src/components/Camera/AICamera/AICamera.js b/src/components/Camera/AICamera/AICamera.js index 9ece171c5..7198bce75 100644 --- a/src/components/Camera/AICamera/AICamera.js +++ b/src/components/Camera/AICamera/AICamera.js @@ -4,8 +4,8 @@ import { useNavigation } from "@react-navigation/native"; import classnames from "classnames"; import FadeInOutView from "components/Camera/FadeInOutView"; import useRotation from "components/Camera/hooks/useRotation"; -import useTakePhoto from "components/Camera/hooks/useTakePhoto"; -import useZoom from "components/Camera/hooks/useZoom"; +import useTakePhoto from "components/Camera/hooks/useTakePhoto.ts"; +import useZoom from "components/Camera/hooks/useZoom.ts"; import { Body1, INatIcon, TaxonResult } from "components/SharedComponents"; import { View } from "components/styledComponents"; import type { Node } from "react"; diff --git a/src/components/Camera/AICamera/FrameProcessorCamera.js b/src/components/Camera/AICamera/FrameProcessorCamera.js index aebd8b6eb..9e3a3c326 100644 --- a/src/components/Camera/AICamera/FrameProcessorCamera.js +++ b/src/components/Camera/AICamera/FrameProcessorCamera.js @@ -1,6 +1,6 @@ // @flow import { useNavigation } from "@react-navigation/native"; -import CameraView from "components/Camera/CameraView"; +import CameraView from "components/Camera/CameraView.tsx"; import type { Node } from "react"; import React, { useEffect, diff --git a/src/components/Camera/CameraContainer.js b/src/components/Camera/CameraContainer.tsx similarity index 65% rename from src/components/Camera/CameraContainer.js rename to src/components/Camera/CameraContainer.tsx index 7e27e5c09..00cb76234 100644 --- a/src/components/Camera/CameraContainer.js +++ b/src/components/Camera/CameraContainer.tsx @@ -1,5 +1,3 @@ -// @flow - import { useRoute } from "@react-navigation/native"; import type { Node } from "react"; import React, { @@ -15,12 +13,15 @@ const CameraContainer = ( ): Node => { const { params } = useRoute( ); const addEvidence = params?.addEvidence; const cameraType = params?.camera; - const [cameraPosition, setCameraPosition] = useState( "back" ); - const device = useCameraDevice( cameraPosition ); - - if ( !device ) { - return null; - } + const [cameraPosition, setCameraPosition] = useState<"front" | "back">( "back" ); + // https://react-native-vision-camera.com/docs/guides/devices#selecting-multi-cams + const device = useCameraDevice( cameraPosition, { + physicalDevices: [ + "ultra-wide-angle-camera", + "wide-angle-camera", + "telephoto-camera" + ] + } ); return ( { const [focusAvailable, setFocusAvailable] = useState( true ); @@ -54,6 +56,8 @@ const CameraView = ( { // check if camera page is active const isFocused = useIsFocused( ); + const appState = useAppState( ); + const isActive = isFocused && appState === "active"; // Select a format that provides the highest resolution for photos and videos const iosFormat = useCameraFormat( device, [ @@ -98,8 +102,7 @@ const CameraView = ( { .onStart( e => singleTapToFocus( e ) ); const onError = useCallback( - error => { - // error is a CameraRuntimeError = + ( error: CameraRuntimeError ) => { // { code: string, message: string, cause?: {} } console.log( "error", error ); // If there is no error code, log the error @@ -177,22 +180,23 @@ const CameraView = ( { onError( e )} + exposure={exposure} + format={format} + frameProcessor={frameProcessor} + isActive={isActive} + onError={onError} // react-native-vision-camera v3.9.0: This prop is undocumented, but does work on iOS // it does nothing on Android so we set it to null there orientation={orientationPatch( deviceOrientation )} + photo photoQualityBalance="quality" - frameProcessor={frameProcessor} pixelFormat="yuv" - animatedProps={animatedProps} resizeMode={resizeMode || "cover"} + style={StyleSheet.absoluteFill} + /> ( null ); const { deviceOrientation } = useDeviceOrientation( ); const [addPhotoPermissionResult, setAddPhotoPermissionResult] = useState( null ); const [checkmarkTapped, setCheckmarkTapped] = useState( false ); diff --git a/src/components/Camera/StandardCamera/StandardCamera.js b/src/components/Camera/StandardCamera/StandardCamera.js index d1182fc8d..2e1d003fc 100644 --- a/src/components/Camera/StandardCamera/StandardCamera.js +++ b/src/components/Camera/StandardCamera/StandardCamera.js @@ -2,11 +2,11 @@ import { useFocusEffect, useNavigation, useRoute } from "@react-navigation/native"; import classnames from "classnames"; -import CameraView from "components/Camera/CameraView"; +import CameraView from "components/Camera/CameraView.tsx"; import FadeInOutView from "components/Camera/FadeInOutView"; import useRotation from "components/Camera/hooks/useRotation"; -import useTakePhoto from "components/Camera/hooks/useTakePhoto"; -import useZoom from "components/Camera/hooks/useZoom"; +import useTakePhoto from "components/Camera/hooks/useTakePhoto.ts"; +import useZoom from "components/Camera/hooks/useZoom.ts"; import navigateToObsDetails from "components/ObsDetails/helpers/navigateToObsDetails"; import { View } from "components/styledComponents"; import { getCurrentRoute } from "navigation/navigationUtils"; diff --git a/src/components/Camera/hooks/useTakePhoto.js b/src/components/Camera/hooks/useTakePhoto.ts similarity index 91% rename from src/components/Camera/hooks/useTakePhoto.js rename to src/components/Camera/hooks/useTakePhoto.ts index abc788b49..2f421b654 100644 --- a/src/components/Camera/hooks/useTakePhoto.js +++ b/src/components/Camera/hooks/useTakePhoto.ts @@ -1,8 +1,7 @@ -// @flow - import { useState } from "react"; +import type { PhotoFile } from "react-native-vision-camera"; import Photo from "realmModels/Photo"; import { rotatePhotoPatch, @@ -12,7 +11,7 @@ import { import useDeviceOrientation from "sharedHooks/useDeviceOrientation"; import useStore from "stores/useStore"; -const useTakePhoto = ( camera: Object, addEvidence: ?boolean, device: Object ): Object => { +const useTakePhoto = ( camera: Object, addEvidence: boolean, device: Object ): Object => { const currentObservation = useStore( state => state.currentObservation ); const { deviceOrientation } = useDeviceOrientation( ); const hasFlash = device?.hasFlash; @@ -30,7 +29,7 @@ const useTakePhoto = ( camera: Object, addEvidence: ?boolean, device: Object ): const takePhoto = async ( ) => { setTakingPhoto( true ); - const cameraPhoto = await camera.current.takePhoto( takePhotoOptions ); + const cameraPhoto: PhotoFile = await camera.current.takePhoto( takePhotoOptions ); // Rotate the original photo depending on device orientation const photoRotation = rotationTempPhotoPatch( cameraPhoto, deviceOrientation ); diff --git a/src/components/Camera/hooks/useZoom.js b/src/components/Camera/hooks/useZoom.ts similarity index 94% rename from src/components/Camera/hooks/useZoom.js rename to src/components/Camera/hooks/useZoom.ts index 0aee6d0af..1b177c8d2 100644 --- a/src/components/Camera/hooks/useZoom.js +++ b/src/components/Camera/hooks/useZoom.ts @@ -1,5 +1,3 @@ -// @flow - import { useState } from "react"; @@ -10,6 +8,7 @@ import { useSharedValue, withSpring } from "react-native-reanimated"; +import type { CameraProps } from "react-native-vision-camera"; // This is taken from react-native-vision library itself: https://github.com/mrousavy/react-native-vision-camera/blob/9eed89aac6155eba155595f3e006707152550d0d/package/example/src/Constants.ts#L19 https://github.com/mrousavy/react-native-vision-camera/blob/9eed89aac6155eba155595f3e006707152550d0d/package/example/src/CameraPage.tsx#L34 // The maximum zoom factor you should be able to zoom in @@ -67,7 +66,7 @@ const useZoom = ( device: Object ): Object => { zoom.value = newZoom; }; - const animatedProps = useAnimatedProps( + const animatedProps = useAnimatedProps < CameraProps >( () => ( { zoom: zoom.value } ), [zoom] ); diff --git a/src/navigation/StackNavigators/NoBottomTabStackNavigator.js b/src/navigation/StackNavigators/NoBottomTabStackNavigator.js index d89246cbc..5185c5b2e 100644 --- a/src/navigation/StackNavigators/NoBottomTabStackNavigator.js +++ b/src/navigation/StackNavigators/NoBottomTabStackNavigator.js @@ -2,7 +2,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import CameraContainer from "components/Camera/CameraContainer"; +import CameraContainer from "components/Camera/CameraContainer.tsx"; import GroupPhotosContainer from "components/PhotoImporter/GroupPhotosContainer"; import PhotoGallery from "components/PhotoImporter/PhotoGallery"; import { Heading4 } from "components/SharedComponents"; diff --git a/tests/unit/components/Camera/AICamera.test.js b/tests/unit/components/Camera/AICamera.test.js index dae214405..ba74d8996 100644 --- a/tests/unit/components/Camera/AICamera.test.js +++ b/tests/unit/components/Camera/AICamera.test.js @@ -49,7 +49,7 @@ jest.mock( "components/Camera/AICamera/hooks/usePredictions", () => ( { default: () => mockModelLoaded } ) ); -jest.mock( "components/Camera/hooks/useZoom", () => ( { +jest.mock( "components/Camera/hooks/useZoom.ts", () => ( { __esModule: true, default: () => ( { animatedProps: {} diff --git a/tests/unit/components/Camera/CameraContainer.test.js b/tests/unit/components/Camera/CameraContainer.test.js index edffbf3cb..672f27cdb 100644 --- a/tests/unit/components/Camera/CameraContainer.test.js +++ b/tests/unit/components/Camera/CameraContainer.test.js @@ -1,7 +1,7 @@ import { fireEvent, render, screen } from "@testing-library/react-native"; -import CameraContainer from "components/Camera/CameraContainer"; +import CameraContainer from "components/Camera/CameraContainer.tsx"; import INatPaperProvider from "providers/INatPaperProvider"; import React from "react"; import { View } from "react-native"; @@ -32,7 +32,7 @@ jest.mock( "@react-navigation/native", () => { } ); const mockView = ; -jest.mock( "components/Camera/CameraView", () => ( { +jest.mock( "components/Camera/CameraView.tsx", () => ( { __esModule: true, default: ( ) => mockView } ) );