mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
Obsedit add photo with StandardCamera, two fixes (#1677)
* Reorder hooks dependencies * Return uri from take photo hook * Keep state of photos added in this instance of the camera * List2 TS * INatIconButton TS * Refactor useBackPress to show discard modal only if photos taken during this instance of the camera * Remove newPhotoCount var * TS refactors * fetchUserLocation TS * Increase timeout * Fix error * Hoist deletePhotoByUri * Delete photos on discard * Reorder code * Set saving photo on checkmark press Closes #1556 * Update snapshots * Remove delete test * Create StandardCamera.test.js * Check if image is there before deletion * Update react-native-share-menu+6.0.0.patch * Update e2e_ios.yml * Update some types
This commit is contained in:
4
.github/workflows/e2e_ios.yml
vendored
4
.github/workflows/e2e_ios.yml
vendored
@@ -16,7 +16,7 @@ concurrency:
|
||||
jobs:
|
||||
checksecret:
|
||||
name: check for oauth client
|
||||
runs-on: macos-14
|
||||
runs-on: macos-13
|
||||
outputs:
|
||||
is_SECRETS_PRESENT_set: ${{ steps.checksecret_job.outputs.is_SECRETS_PRESENT_set }}
|
||||
steps:
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
name: Notify Slack
|
||||
needs: test
|
||||
if: ${{ success() || failure() }}
|
||||
runs-on: macos-14
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- uses: iRoachie/slack-github-actions@v2.3.0
|
||||
if: env.SLACK_WEBHOOK_URL != null
|
||||
|
||||
@@ -8,7 +8,7 @@ index 9557fdb..ebdeb6f 100644
|
||||
android {
|
||||
- compileSdkVersion 29
|
||||
- buildToolsVersion "29.0.2"
|
||||
+ compileSdkVersion 31
|
||||
+ compileSdkVersion 33
|
||||
+ buildToolsVersion "31.0.0"
|
||||
|
||||
defaultConfig {
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import classnames from "classnames";
|
||||
import MediaViewerModal from "components/MediaViewer/MediaViewerModal";
|
||||
import { ActivityIndicator, INatIconButton } from "components/SharedComponents";
|
||||
import { ImageBackground, Pressable, View } from "components/styledComponents";
|
||||
import { RealmContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, {
|
||||
useCallback, useEffect, useRef, useState
|
||||
} from "react";
|
||||
@@ -18,53 +14,49 @@ import Animated, {
|
||||
useAnimatedStyle,
|
||||
withTiming
|
||||
} from "react-native-reanimated";
|
||||
import ObservationPhoto from "realmModels/ObservationPhoto";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
import useStore from "stores/useStore";
|
||||
|
||||
const { useRealm } = RealmContext;
|
||||
|
||||
type Props = {
|
||||
emptyComponent?: Function,
|
||||
takingPhoto?: boolean,
|
||||
isLandscapeMode?:boolean,
|
||||
isLargeScreen?: boolean,
|
||||
isTablet?: boolean,
|
||||
interface Props {
|
||||
takingPhoto?: boolean;
|
||||
isLandscapeMode?: boolean;
|
||||
isLargeScreen?: boolean;
|
||||
isTablet?: boolean;
|
||||
rotation?: {
|
||||
value: number
|
||||
},
|
||||
photoUris: Array<string>
|
||||
};
|
||||
photoUris: string[];
|
||||
onDelete: ( _uri: string ) => void;
|
||||
}
|
||||
|
||||
export const SMALL_PHOTO_DIM = 42;
|
||||
export const LARGE_PHOTO_DIM = 83;
|
||||
export const SMALL_PHOTO_GUTTER = 6;
|
||||
export const LARGE_PHOTO_GUTTER = 17;
|
||||
const IMAGE_CONTAINER_CLASSES = ["justify-center", "items-center"];
|
||||
const IMAGE_CONTAINER_CLASSES = ["justify-center", "items-center"] as const;
|
||||
const SMALL_PHOTO_CLASSES = [
|
||||
"rounded-sm",
|
||||
"w-[42px]",
|
||||
"h-[42x]",
|
||||
"mx-[3px]"
|
||||
];
|
||||
] as const;
|
||||
const LARGE_PHOTO_CLASSES = [
|
||||
"rounded-md",
|
||||
"w-[83px]",
|
||||
"h-[83px]",
|
||||
"m-[8.5px]"
|
||||
];
|
||||
] as const;
|
||||
|
||||
const PhotoCarousel = ( {
|
||||
emptyComponent,
|
||||
takingPhoto,
|
||||
isLandscapeMode,
|
||||
isLargeScreen,
|
||||
isTablet,
|
||||
rotation,
|
||||
photoUris
|
||||
}: Props ): Node => {
|
||||
photoUris,
|
||||
onDelete
|
||||
}: Props ) => {
|
||||
const deletePhotoFromObservation = useStore( state => state.deletePhotoFromObservation );
|
||||
const realm = useRealm( );
|
||||
const { t } = useTranslation( );
|
||||
const theme = useTheme( );
|
||||
const [deletePhotoMode, setDeletePhotoMode] = useState( false );
|
||||
@@ -92,7 +84,7 @@ const PhotoCarousel = ( {
|
||||
{
|
||||
rotateZ: rotation
|
||||
? withTiming( `${-1 * rotation.value}deg` )
|
||||
: 0
|
||||
: "0"
|
||||
}
|
||||
]
|
||||
} ),
|
||||
@@ -133,19 +125,16 @@ const PhotoCarousel = ( {
|
||||
}
|
||||
}, [deletePhotoFromObservation] );
|
||||
|
||||
const viewPhotoAtIndex = useCallback( ( item, index ) => {
|
||||
const viewPhotoAtIndex = useCallback( ( index: number ) => {
|
||||
setTappedPhotoIndex( index );
|
||||
}, [
|
||||
setTappedPhotoIndex
|
||||
] );
|
||||
|
||||
const deletePhotoByUri = useCallback( async photoUri => {
|
||||
if ( !deletePhotoFromObservation ) return;
|
||||
deletePhotoFromObservation( photoUri );
|
||||
await ObservationPhoto.deletePhoto( realm, photoUri );
|
||||
}, [deletePhotoFromObservation, realm] );
|
||||
|
||||
const renderPhotoOrEvidenceButton = useCallback( ( { item: photoUri, index } ) => (
|
||||
const renderPhotoOrEvidenceButton = useCallback( ( {
|
||||
item: photoUri,
|
||||
index
|
||||
} ) => (
|
||||
<>
|
||||
{index === 0 && renderSkeleton( )}
|
||||
<Animated.View style={!isTablet && animatedStyle}>
|
||||
@@ -181,7 +170,7 @@ const PhotoCarousel = ( {
|
||||
backgroundColor="rgba(0, 0, 0, 0.5)"
|
||||
testID={`PhotoCarousel.deletePhoto.${photoUri}`}
|
||||
accessibilityLabel={t( "Delete-photo" )}
|
||||
onPress={( ) => deletePhotoByUri( photoUri )}
|
||||
onPress={( ) => onDelete( photoUri )}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
@@ -191,7 +180,7 @@ const PhotoCarousel = ( {
|
||||
accessibilityLabel={t( "View-photo" )}
|
||||
testID={`PhotoCarousel.displayPhoto.${photoUri}`}
|
||||
onLongPress={showDeletePhotoMode}
|
||||
onPress={( ) => viewPhotoAtIndex( photoUri, index )}
|
||||
onPress={( ) => viewPhotoAtIndex( index )}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
)
|
||||
@@ -203,7 +192,7 @@ const PhotoCarousel = ( {
|
||||
</>
|
||||
), [
|
||||
animatedStyle,
|
||||
deletePhotoByUri,
|
||||
onDelete,
|
||||
deletePhotoMode,
|
||||
isTablet,
|
||||
photoClasses,
|
||||
@@ -221,7 +210,7 @@ const PhotoCarousel = ( {
|
||||
horizontal={!isTablet || !isLandscapeMode}
|
||||
ListEmptyComponent={takingPhoto
|
||||
? renderSkeleton( )
|
||||
: emptyComponent}
|
||||
: null}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -235,8 +224,13 @@ const PhotoCarousel = ( {
|
||||
// state, and use that to position another container inside the modal in
|
||||
// exactly the same place
|
||||
|
||||
const containerRef = useRef( );
|
||||
const [containerPos, setContainerPos] = useState( { x: null, y: null } );
|
||||
const containerRef = useRef<View>( );
|
||||
const [containerPos, setContainerPos] = useState<{
|
||||
x: number | null;
|
||||
y: number | null;
|
||||
w?: number;
|
||||
h?: number;
|
||||
}>( { x: null, y: null } );
|
||||
const containerStyle = {
|
||||
height: isTablet && isLandscapeMode
|
||||
? photoUris.length * ( photoDim + photoGutter ) + photoGutter
|
||||
@@ -299,8 +293,8 @@ const PhotoCarousel = ( {
|
||||
editable
|
||||
showModal={tappedPhotoIndex >= 0}
|
||||
onClose={( ) => setTappedPhotoIndex( -1 )}
|
||||
onDeletePhoto={async photoUri => {
|
||||
await deletePhotoByUri( photoUri );
|
||||
onDeletePhoto={async ( photoUri: string ) => {
|
||||
await onDelete( photoUri );
|
||||
setTappedPhotoIndex( tappedPhotoIndex - 1 );
|
||||
}}
|
||||
uri={photoUris[tappedPhotoIndex]}
|
||||
@@ -1,8 +1,6 @@
|
||||
// @flow
|
||||
import classnames from "classnames";
|
||||
import { Subheading1 } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
|
||||
@@ -13,22 +11,23 @@ import PhotoCarousel, {
|
||||
SMALL_PHOTO_GUTTER
|
||||
} from "./PhotoCarousel";
|
||||
|
||||
type Props = {
|
||||
interface Props {
|
||||
rotation?: {
|
||||
value: number
|
||||
},
|
||||
isLandscapeMode?: boolean,
|
||||
isLargeScreen?: boolean,
|
||||
isTablet?: boolean,
|
||||
takingPhoto: boolean,
|
||||
rotatedOriginalCameraPhotos: Function,
|
||||
};
|
||||
isLandscapeMode?: boolean;
|
||||
isLargeScreen?: boolean;
|
||||
isTablet?: boolean;
|
||||
takingPhoto: boolean;
|
||||
rotatedOriginalCameraPhotos: string[];
|
||||
onDelete: ( _uri: string ) => void;
|
||||
}
|
||||
|
||||
const STYLE = {
|
||||
justifyContent: "center",
|
||||
flex: 0,
|
||||
flexShrink: 1
|
||||
};
|
||||
} as const;
|
||||
|
||||
const PhotoPreview = ( {
|
||||
isLandscapeMode,
|
||||
@@ -36,8 +35,9 @@ const PhotoPreview = ( {
|
||||
isTablet,
|
||||
rotation,
|
||||
takingPhoto,
|
||||
rotatedOriginalCameraPhotos
|
||||
}: Props ): Node => {
|
||||
rotatedOriginalCameraPhotos,
|
||||
onDelete
|
||||
}: Props ) => {
|
||||
const { t } = useTranslation( );
|
||||
const wrapperDim = isLargeScreen
|
||||
? LARGE_PHOTO_DIM + LARGE_PHOTO_GUTTER * 2
|
||||
@@ -74,7 +74,10 @@ const PhotoPreview = ( {
|
||||
);
|
||||
}
|
||||
|
||||
const dynamicStyle = {};
|
||||
const dynamicStyle: {
|
||||
width?: number | string;
|
||||
height?: number;
|
||||
} = {};
|
||||
if ( isTablet && isLandscapeMode ) {
|
||||
dynamicStyle.width = wrapperDim;
|
||||
} else {
|
||||
@@ -84,7 +87,6 @@ const PhotoPreview = ( {
|
||||
|
||||
return (
|
||||
<View
|
||||
// eslint-disable-next-line react-native/no-inline-styles
|
||||
style={[STYLE, dynamicStyle]}
|
||||
>
|
||||
{
|
||||
@@ -98,6 +100,7 @@ const PhotoPreview = ( {
|
||||
isLargeScreen={isLargeScreen}
|
||||
isTablet={isTablet}
|
||||
isLandscapeMode={isLandscapeMode}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import { useFocusEffect, useNavigation, useRoute } from "@react-navigation/native";
|
||||
import { useFocusEffect, useNavigation } from "@react-navigation/native";
|
||||
import classnames from "classnames";
|
||||
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.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";
|
||||
import { RealmContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
@@ -19,6 +18,7 @@ import React, {
|
||||
} from "react";
|
||||
import DeviceInfo from "react-native-device-info";
|
||||
import { Snackbar } from "react-native-paper";
|
||||
import ObservationPhoto from "realmModels/ObservationPhoto";
|
||||
import { BREAKPOINTS } from "sharedHelpers/breakpoint";
|
||||
import useDeviceOrientation from "sharedHooks/useDeviceOrientation";
|
||||
import useTranslation from "sharedHooks/useTranslation";
|
||||
@@ -36,6 +36,8 @@ import DiscardChangesSheet from "./DiscardChangesSheet";
|
||||
import useBackPress from "./hooks/useBackPress";
|
||||
import PhotoPreview from "./PhotoPreview";
|
||||
|
||||
const { useRealm } = RealmContext;
|
||||
|
||||
const isTablet = DeviceInfo.isTablet( );
|
||||
|
||||
export const MAX_PHOTOS_ALLOWED = 20;
|
||||
@@ -57,6 +59,7 @@ const StandardCamera = ( {
|
||||
handleCheckmarkPress,
|
||||
isLandscapeMode
|
||||
}: Props ): Node => {
|
||||
const realm = useRealm( );
|
||||
const hasFlash = device?.hasFlash;
|
||||
const {
|
||||
animatedProps,
|
||||
@@ -71,33 +74,6 @@ const StandardCamera = ( {
|
||||
rotation
|
||||
} = useRotation( );
|
||||
const navigation = useNavigation( );
|
||||
const { params } = useRoute();
|
||||
const onBack = () => {
|
||||
const currentRoute = getCurrentRoute();
|
||||
if ( currentRoute?.params?.addEvidence ) {
|
||||
navigation.navigate( "ObsEdit" );
|
||||
} else {
|
||||
const previousScreen = params && params.previousScreen
|
||||
? params.previousScreen
|
||||
: null;
|
||||
|
||||
if ( previousScreen && previousScreen.name === "ObsDetails" ) {
|
||||
navigateToObsDetails( navigation, previousScreen.params.uuid );
|
||||
} else {
|
||||
navigation.navigate( "TabNavigator", {
|
||||
screen: "TabStackNavigator",
|
||||
params: {
|
||||
screen: "ObsList"
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
};
|
||||
const {
|
||||
handleBackButtonPress,
|
||||
setShowDiscardSheet,
|
||||
showDiscardSheet
|
||||
} = useBackPress( onBack );
|
||||
const {
|
||||
takePhoto,
|
||||
takePhotoOptions,
|
||||
@@ -110,6 +86,7 @@ const StandardCamera = ( {
|
||||
const rotatedOriginalCameraPhotos = useStore( state => state.rotatedOriginalCameraPhotos );
|
||||
const resetEvidenceToAdd = useStore( state => state.resetEvidenceToAdd );
|
||||
const galleryUris = useStore( state => state.galleryUris );
|
||||
const deletePhotoFromObservation = useStore( state => state.deletePhotoFromObservation );
|
||||
|
||||
const totalObsPhotoUris = useMemo(
|
||||
( ) => [...rotatedOriginalCameraPhotos, ...galleryUris].length,
|
||||
@@ -119,25 +96,37 @@ const StandardCamera = ( {
|
||||
const disallowAddingPhotos = totalObsPhotoUris >= MAX_PHOTOS_ALLOWED;
|
||||
const [showAlert, setShowAlert] = useState( false );
|
||||
const [dismissChanges, setDismissChanges] = useState( false );
|
||||
const [newPhotoCount, setNewPhotoCount] = useState( 0 );
|
||||
const [newPhotoUris, setNewPhotoUris] = useState( [] );
|
||||
|
||||
const { screenWidth } = useDeviceOrientation( );
|
||||
|
||||
// newPhotoCount tracks photos taken in *this* instance of the camera. The
|
||||
// newPhotoUris tracks photos taken in *this* instance of the camera. The
|
||||
// camera might be instantiated with several rotatedOriginalCameraPhotos or
|
||||
// galleryUris already in state, but we only want to show the CTA button
|
||||
// galleryUris already in state, but we only want to show the CTA button or discard modal
|
||||
// when the user has taken a photo with *this* instance of the camera
|
||||
const photosTaken = newPhotoCount > 0 && totalObsPhotoUris > 0;
|
||||
const photosTaken = newPhotoUris.length > 0 && totalObsPhotoUris > 0;
|
||||
const {
|
||||
handleBackButtonPress,
|
||||
setShowDiscardSheet,
|
||||
showDiscardSheet
|
||||
} = useBackPress( photosTaken );
|
||||
|
||||
useFocusEffect(
|
||||
useCallback( ( ) => {
|
||||
// Reset camera zoom every time we get into a fresh camera view
|
||||
resetZoom( );
|
||||
resetEvidenceToAdd( );
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [] )
|
||||
);
|
||||
|
||||
const deletePhotoByUri = useCallback( async ( photoUri: string ) => {
|
||||
if ( !deletePhotoFromObservation ) return;
|
||||
deletePhotoFromObservation( photoUri );
|
||||
await ObservationPhoto.deletePhoto( realm, photoUri );
|
||||
setNewPhotoUris( newPhotoUris.filter( uri => uri !== photoUri ) );
|
||||
}, [deletePhotoFromObservation, realm, newPhotoUris] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
// We do this navigation indirectly (vs doing it directly in DiscardChangesSheet),
|
||||
// since we need for the bottom sheet of discard-changes to first finish dismissing,
|
||||
@@ -145,19 +134,21 @@ const StandardCamera = ( {
|
||||
// to sometimes pop back up on the next screen - see GH issue #629
|
||||
if ( !showDiscardSheet ) {
|
||||
if ( dismissChanges ) {
|
||||
// TODO delete any new photos taken
|
||||
newPhotoUris.forEach( uri => {
|
||||
deletePhotoByUri( uri );
|
||||
} );
|
||||
navigation.goBack();
|
||||
}
|
||||
}
|
||||
}, [dismissChanges, showDiscardSheet, navigation] );
|
||||
}, [dismissChanges, showDiscardSheet, navigation, newPhotoUris, deletePhotoByUri] );
|
||||
|
||||
const handleTakePhoto = async ( ) => {
|
||||
if ( disallowAddingPhotos ) {
|
||||
setShowAlert( true );
|
||||
return;
|
||||
}
|
||||
await takePhoto( );
|
||||
setNewPhotoCount( newPhotoCount + 1 );
|
||||
const uri = await takePhoto( );
|
||||
setNewPhotoUris( [...newPhotoUris, uri] );
|
||||
};
|
||||
|
||||
const containerClasses = ["flex-1"];
|
||||
@@ -174,6 +165,7 @@ const StandardCamera = ( {
|
||||
isLargeScreen={screenWidth > BREAKPOINTS.md}
|
||||
isTablet={isTablet}
|
||||
rotatedOriginalCameraPhotos={rotatedOriginalCameraPhotos}
|
||||
onDelete={deletePhotoByUri}
|
||||
/>
|
||||
<View className="relative flex-1">
|
||||
{device && (
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import {
|
||||
useCallback,
|
||||
useState
|
||||
} from "react";
|
||||
import {
|
||||
BackHandler
|
||||
} from "react-native";
|
||||
import useStore from "stores/useStore";
|
||||
|
||||
const useBackPress = ( onBack: Function ): Object => {
|
||||
const [showDiscardSheet, setShowDiscardSheet] = useState( false );
|
||||
const rotatedOriginalCameraPhotos = useStore( state => state.rotatedOriginalCameraPhotos );
|
||||
|
||||
const handleBackButtonPress = useCallback( ( ) => {
|
||||
if ( rotatedOriginalCameraPhotos.length > 0 ) {
|
||||
setShowDiscardSheet( true );
|
||||
} else {
|
||||
onBack();
|
||||
}
|
||||
}, [setShowDiscardSheet, rotatedOriginalCameraPhotos, onBack] );
|
||||
|
||||
useFocusEffect(
|
||||
// note: cannot use navigation.addListener to trigger bottom sheet in tab navigator
|
||||
// since the screen is unfocused, not removed from navigation
|
||||
useCallback( ( ) => {
|
||||
// make sure an Android user cannot back out and accidentally discard photos
|
||||
const onBackPress = ( ) => {
|
||||
handleBackButtonPress( );
|
||||
return true;
|
||||
};
|
||||
|
||||
BackHandler.addEventListener( "hardwareBackPress", onBackPress );
|
||||
|
||||
return ( ) => BackHandler.removeEventListener( "hardwareBackPress", onBackPress );
|
||||
}, [handleBackButtonPress] )
|
||||
);
|
||||
|
||||
return {
|
||||
handleBackButtonPress,
|
||||
setShowDiscardSheet,
|
||||
showDiscardSheet
|
||||
};
|
||||
};
|
||||
|
||||
export default useBackPress;
|
||||
67
src/components/Camera/StandardCamera/hooks/useBackPress.ts
Normal file
67
src/components/Camera/StandardCamera/hooks/useBackPress.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useFocusEffect, useNavigation, useRoute } from "@react-navigation/native";
|
||||
import navigateToObsDetails from "components/ObsDetails/helpers/navigateToObsDetails";
|
||||
import { getCurrentRoute } from "navigation/navigationUtils.ts";
|
||||
import {
|
||||
useCallback,
|
||||
useState
|
||||
} from "react";
|
||||
import {
|
||||
BackHandler
|
||||
} from "react-native";
|
||||
|
||||
const useBackPress = ( shouldShowDiscardSheet: boolean ) => {
|
||||
const navigation = useNavigation( );
|
||||
const { params } = useRoute();
|
||||
|
||||
const [showDiscardSheet, setShowDiscardSheet] = useState( false );
|
||||
|
||||
const handleBackButtonPress = useCallback( ( ) => {
|
||||
if ( shouldShowDiscardSheet ) {
|
||||
setShowDiscardSheet( true );
|
||||
} else {
|
||||
const currentRoute = getCurrentRoute();
|
||||
if ( currentRoute?.params?.addEvidence ) {
|
||||
navigation.navigate( "ObsEdit" );
|
||||
} else {
|
||||
const previousScreen = params && params.previousScreen
|
||||
? params.previousScreen
|
||||
: null;
|
||||
|
||||
if ( previousScreen && previousScreen.name === "ObsDetails" ) {
|
||||
navigateToObsDetails( navigation, previousScreen.params.uuid );
|
||||
} else {
|
||||
navigation.navigate( "TabNavigator", {
|
||||
screen: "TabStackNavigator",
|
||||
params: {
|
||||
screen: "ObsList"
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [shouldShowDiscardSheet, setShowDiscardSheet, navigation, params] );
|
||||
|
||||
useFocusEffect(
|
||||
// note: cannot use navigation.addListener to trigger bottom sheet in tab navigator
|
||||
// since the screen is unfocused, not removed from navigation
|
||||
useCallback( ( ) => {
|
||||
// make sure an Android user cannot back out and accidentally discard photos
|
||||
const onBackPress = ( ) => {
|
||||
handleBackButtonPress( );
|
||||
return true;
|
||||
};
|
||||
|
||||
BackHandler.addEventListener( "hardwareBackPress", onBackPress );
|
||||
|
||||
return ( ) => BackHandler.removeEventListener( "hardwareBackPress", onBackPress );
|
||||
}, [handleBackButtonPress] )
|
||||
);
|
||||
|
||||
return {
|
||||
handleBackButtonPress,
|
||||
setShowDiscardSheet,
|
||||
showDiscardSheet
|
||||
};
|
||||
};
|
||||
|
||||
export default useBackPress;
|
||||
@@ -36,6 +36,7 @@ const usePrepareStoreAndNavigate = ( options: Options ): Function => {
|
||||
const addCameraRollUri = useStore( state => state.addCameraRollUri );
|
||||
const currentObservationIndex = useStore( state => state.currentObservationIndex );
|
||||
const observations = useStore( state => state.observations );
|
||||
const setSavingPhoto = useStore( state => state.setSavingPhoto );
|
||||
const { userLocation } = useUserLocation( { untilAcc: 5, enabled: !!shouldFetchLocation } );
|
||||
|
||||
const numOfObsPhotos = currentObservation?.observationPhotos?.length || 0;
|
||||
@@ -137,6 +138,7 @@ const usePrepareStoreAndNavigate = ( options: Options ): Function => {
|
||||
const prepareStoreAndNavigate = useCallback( async ( visionResult = null ) => {
|
||||
if ( !checkmarkTapped ) { return null; }
|
||||
|
||||
setSavingPhoto( true );
|
||||
// save all to camera roll
|
||||
|
||||
// handle case where user backs out from ObsEdit -> Suggestions -> Camera
|
||||
@@ -163,7 +165,8 @@ const usePrepareStoreAndNavigate = ( options: Options ): Function => {
|
||||
createObsWithCameraPhotos,
|
||||
currentObservation,
|
||||
navigation,
|
||||
updateObsWithCameraPhotos
|
||||
updateObsWithCameraPhotos,
|
||||
setSavingPhoto
|
||||
] );
|
||||
|
||||
return prepareStoreAndNavigate;
|
||||
|
||||
@@ -2,6 +2,9 @@ import { RealmContext } from "providers/contexts";
|
||||
import {
|
||||
useState
|
||||
} from "react";
|
||||
import {
|
||||
Camera, CameraDevice, PhotoFile, TakePhotoOptions
|
||||
} from "react-native-vision-camera";
|
||||
import ObservationPhoto from "realmModels/ObservationPhoto";
|
||||
import {
|
||||
rotatePhotoPatch,
|
||||
@@ -12,24 +15,29 @@ import useStore from "stores/useStore";
|
||||
|
||||
const { useRealm } = RealmContext;
|
||||
|
||||
const useTakePhoto = ( camera: Object, addEvidence?: boolean, device?: Object ): Object => {
|
||||
const useTakePhoto = (
|
||||
camera: React.RefObject<Camera>,
|
||||
addEvidence?: boolean,
|
||||
device?: CameraDevice
|
||||
): Object => {
|
||||
const realm = useRealm( );
|
||||
const currentObservation = useStore( state => state.currentObservation );
|
||||
const { deviceOrientation } = useDeviceOrientation( );
|
||||
const hasFlash = device?.hasFlash;
|
||||
const initialPhotoOptions = {
|
||||
enableShutterSound: true,
|
||||
...( hasFlash && { flash: "off" } )
|
||||
};
|
||||
const deletePhotoFromObservation = useStore( state => state.deletePhotoFromObservation );
|
||||
const [takePhotoOptions, setTakePhotoOptions] = useState( initialPhotoOptions );
|
||||
const [takingPhoto, setTakingPhoto] = useState( false );
|
||||
|
||||
const currentObservation = useStore( state => state.currentObservation );
|
||||
const deletePhotoFromObservation = useStore( state => state.deletePhotoFromObservation );
|
||||
const setCameraState = useStore( state => state.setCameraState );
|
||||
const evidenceToAdd = useStore( state => state.evidenceToAdd );
|
||||
const rotatedOriginalCameraPhotos = useStore( state => state.rotatedOriginalCameraPhotos );
|
||||
|
||||
const saveRotatedPhotoToDocumentsDirectory = async cameraPhoto => {
|
||||
const hasFlash = device?.hasFlash;
|
||||
const initialPhotoOptions = {
|
||||
enableShutterSound: true,
|
||||
...( hasFlash && { flash: "off" } as const )
|
||||
} as const;
|
||||
const [takePhotoOptions, setTakePhotoOptions] = useState<TakePhotoOptions>( initialPhotoOptions );
|
||||
const [takingPhoto, setTakingPhoto] = useState( false );
|
||||
|
||||
const saveRotatedPhotoToDocumentsDirectory = async ( cameraPhoto: PhotoFile ) => {
|
||||
// Rotate the original photo depending on device orientation
|
||||
const photoRotation = rotationTempPhotoPatch( cameraPhoto, deviceOrientation );
|
||||
return rotatePhotoPatch( cameraPhoto, photoRotation );
|
||||
@@ -70,6 +78,7 @@ const useTakePhoto = ( camera: Object, addEvidence?: boolean, device?: Object ):
|
||||
const uri = await saveRotatedPhotoToDocumentsDirectory( cameraPhoto );
|
||||
await updateStore( uri, options );
|
||||
setTakingPhoto( false );
|
||||
return uri;
|
||||
};
|
||||
|
||||
const toggleFlash = ( ) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import AddObsModal from "components/AddObsModal";
|
||||
import { Modal } from "components/SharedComponents";
|
||||
import GradientButton from "components/SharedComponents/Buttons/GradientButton";
|
||||
import { t } from "i18next";
|
||||
import { getCurrentRoute } from "navigation/navigationUtils";
|
||||
import { getCurrentRoute } from "navigation/navigationUtils.ts";
|
||||
import * as React from "react";
|
||||
import { log } from "sharedHelpers/logger";
|
||||
import useStore from "stores/useStore";
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
// @flow
|
||||
|
||||
import classnames from "classnames";
|
||||
import { INatIcon } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Platform, Pressable } from "react-native";
|
||||
import {
|
||||
GestureResponderEvent,
|
||||
Platform,
|
||||
Pressable,
|
||||
ViewStyle
|
||||
} from "react-native";
|
||||
import { useTheme } from "react-native-paper";
|
||||
|
||||
type Props = {
|
||||
accessibilityHint?: string,
|
||||
accessibilityLabel: string,
|
||||
// $FlowIgnore
|
||||
children?: unknown,
|
||||
color?: string,
|
||||
disabled?: boolean,
|
||||
height?: number,
|
||||
icon: string,
|
||||
onPress: Function,
|
||||
interface Props {
|
||||
accessibilityHint?: string;
|
||||
accessibilityLabel: string;
|
||||
children?: React.ReactNode;
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
height?: number;
|
||||
icon: string;
|
||||
onPress: ( _event: GestureResponderEvent ) => void;
|
||||
// Inserts a white or colored view under the icon so an holes in the shape show as
|
||||
// white
|
||||
preventTransparency?: boolean,
|
||||
size?: number,
|
||||
style?: Object,
|
||||
testID?: string,
|
||||
width?: number,
|
||||
backgroundColor?: string,
|
||||
mode?: "contained"
|
||||
preventTransparency?: boolean;
|
||||
size?: number;
|
||||
style?: ViewStyle;
|
||||
testID?: string;
|
||||
width?: number;
|
||||
backgroundColor?: string;
|
||||
mode?: "contained";
|
||||
}
|
||||
|
||||
const MIN_ACCESSIBLE_DIM = 44;
|
||||
@@ -50,7 +51,7 @@ const INatIconButton = ( {
|
||||
width = MIN_ACCESSIBLE_DIM,
|
||||
backgroundColor,
|
||||
mode
|
||||
}: Props ): Node => {
|
||||
}: Props ) => {
|
||||
const theme = useTheme( );
|
||||
// width || 0 is to placate flow. width should never be undefined because of
|
||||
// the defaultProps, but I guess flow can't figure that out.
|
||||
@@ -69,7 +70,7 @@ const INatIconButton = ( {
|
||||
"Button needs an accessibility label"
|
||||
);
|
||||
}
|
||||
const opacity = pressed => {
|
||||
const opacity = ( pressed: boolean ) => {
|
||||
if ( disabled ) {
|
||||
return 0.5;
|
||||
}
|
||||
@@ -97,7 +98,7 @@ const INatIconButton = ( {
|
||||
},
|
||||
mode === "contained" && {
|
||||
backgroundColor: preventTransparency
|
||||
? null
|
||||
? undefined
|
||||
: backgroundColor,
|
||||
borderRadius: 9999
|
||||
},
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import INatIconButton from "components/SharedComponents/Buttons/INatIconButton";
|
||||
import INatIconButton from "components/SharedComponents/Buttons/INatIconButton.tsx";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Pressable } from "react-native";
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
tailwindFontRegular
|
||||
} from "appConstants/fontFamilies.ts";
|
||||
import { tailwindFontRegular } from "appConstants/fontFamilies.ts";
|
||||
import classnames from "classnames";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Text } from "react-native";
|
||||
|
||||
const List2 = ( props: Object ): Node => (
|
||||
const List2 = ( props: Object ) => (
|
||||
<Text
|
||||
className={classnames(
|
||||
"text-sm trailing-tight text-darkGray",
|
||||
@@ -1,14 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
tailwindFontRegular
|
||||
} from "appConstants/fontFamilies.ts";
|
||||
import classnames from "classnames";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Text } from "react-native";
|
||||
|
||||
const Subheading1 = ( props: Object ): Node => (
|
||||
const Subheading1 = ( props: Object ) => (
|
||||
<Text
|
||||
className={classnames(
|
||||
"text-xl trailing-tight text-darkGray",
|
||||
@@ -3,8 +3,7 @@
|
||||
import { t } from "i18next";
|
||||
import * as React from "react";
|
||||
import { LatLng } from "react-native-maps";
|
||||
|
||||
import fetchUserLocation from "../sharedHelpers/fetchUserLocation";
|
||||
import fetchUserLocation from "sharedHelpers/fetchUserLocation.ts";
|
||||
|
||||
export enum EXPLORE_ACTION {
|
||||
CHANGE_SORT_BY = "CHANGE_SORT_BY",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Geolocation from "@react-native-community/geolocation";
|
||||
import Geolocation, { GeolocationResponse } from "@react-native-community/geolocation";
|
||||
import {
|
||||
LOCATION_PERMISSIONS,
|
||||
permissionResultFromMultiple
|
||||
@@ -12,20 +12,20 @@ import {
|
||||
|
||||
const options = {
|
||||
enableHighAccuracy: true,
|
||||
maximumAge: 0
|
||||
};
|
||||
maximumAge: 0,
|
||||
timeout: 2000
|
||||
} as const;
|
||||
|
||||
const getCurrentPosition = ( ) => new Promise(
|
||||
const getCurrentPosition = ( ): Promise<GeolocationResponse> => new Promise(
|
||||
( resolve, error ) => {
|
||||
Geolocation.getCurrentPosition( resolve, error, options );
|
||||
}
|
||||
);
|
||||
|
||||
type UserLocation = {
|
||||
latitude: number,
|
||||
longitude: number,
|
||||
positional_accuracy: number
|
||||
|
||||
interface UserLocation {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
positional_accuracy: number;
|
||||
}
|
||||
const fetchUserLocation = async ( ): Promise<UserLocation | null> => {
|
||||
const permissionResult = permissionResultFromMultiple(
|
||||
|
||||
@@ -10,9 +10,7 @@ import {
|
||||
useState
|
||||
} from "react";
|
||||
import { checkMultiple, RESULTS } from "react-native-permissions";
|
||||
|
||||
// For some reason this doesn't work with the path alias *and* a typescript file
|
||||
import fetchUserLocation from "../sharedHelpers/fetchUserLocation";
|
||||
import fetchUserLocation from "sharedHelpers/fetchUserLocation.ts";
|
||||
|
||||
const INITIAL_POSITIONAL_ACCURACY = 99999;
|
||||
const TARGET_POSITIONAL_ACCURACY = 10;
|
||||
|
||||
@@ -23,7 +23,7 @@ const DEFAULT_STATE = {
|
||||
const removeObsPhotoFromObservation = ( currentObservation, uri ) => {
|
||||
if ( _.isEmpty( currentObservation ) ) { return []; }
|
||||
const updatedObservation = currentObservation;
|
||||
const obsPhotos = Array.from( currentObservation?.observationPhotos );
|
||||
const obsPhotos = Array.from( currentObservation?.observationPhotos || [] );
|
||||
if ( obsPhotos.length > 0 ) {
|
||||
// FYI, _.remove edits the array in place and returns the items you
|
||||
// removed
|
||||
@@ -109,11 +109,11 @@ const createObservationFlowSlice = set => ( {
|
||||
savingPhoto: false
|
||||
} );
|
||||
} ),
|
||||
setSavingPhoto: saving => set( { savingPhoto: saving } ),
|
||||
setCameraState: options => set( state => ( {
|
||||
evidenceToAdd: options?.evidenceToAdd || state.evidenceToAdd,
|
||||
rotatedOriginalCameraPhotos:
|
||||
options?.rotatedOriginalCameraPhotos || state.rotatedOriginalCameraPhotos,
|
||||
savingPhoto: options?.evidenceToAdd?.length > 0 || state.savingPhoto
|
||||
options?.rotatedOriginalCameraPhotos || state.rotatedOriginalCameraPhotos
|
||||
} ) ),
|
||||
setCurrentObservationIndex: index => set( state => ( {
|
||||
currentObservationIndex: index,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
fireEvent, render, screen, waitFor
|
||||
render, screen
|
||||
} from "@testing-library/react-native";
|
||||
import PhotoCarousel from "components/Camera/StandardCamera/PhotoCarousel";
|
||||
import PhotoCarousel from "components/Camera/StandardCamera/PhotoCarousel.tsx";
|
||||
import React from "react";
|
||||
import useStore from "stores/useStore";
|
||||
|
||||
@@ -45,56 +45,4 @@ describe( "PhotoCarousel", ( ) => {
|
||||
// Snapshot test
|
||||
expect( screen ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
it( "deletes a photo on long press", async ( ) => {
|
||||
const removePhotoFromList = ( list, photo ) => {
|
||||
const i = list.findIndex( p => p === photo );
|
||||
list.splice( i, 1 );
|
||||
return list || [];
|
||||
};
|
||||
|
||||
useStore.setState( {
|
||||
evidenceToAdd: [mockPhotoUris[2]],
|
||||
rotatedOriginalCameraPhotos: mockPhotoUris,
|
||||
deletePhotoFromObservation: uri => useStore.setState( {
|
||||
rotatedOriginalCameraPhotos: [...removePhotoFromList( mockPhotoUris, uri )]
|
||||
} )
|
||||
} );
|
||||
|
||||
const { rotatedOriginalCameraPhotos } = useStore.getState( );
|
||||
|
||||
render(
|
||||
<PhotoCarousel
|
||||
photoUris={rotatedOriginalCameraPhotos}
|
||||
/>
|
||||
);
|
||||
const photoImage = screen.getByTestId(
|
||||
`PhotoCarousel.displayPhoto.${rotatedOriginalCameraPhotos[2]}`
|
||||
);
|
||||
fireEvent( photoImage, "onLongPress" );
|
||||
const deleteMode = screen.getByTestId(
|
||||
`PhotoCarousel.deletePhoto.${rotatedOriginalCameraPhotos[2]}`
|
||||
);
|
||||
await waitFor( ( ) => {
|
||||
expect( deleteMode ).toBeVisible( );
|
||||
} );
|
||||
fireEvent.press( deleteMode );
|
||||
|
||||
render(
|
||||
<PhotoCarousel
|
||||
photoUris={rotatedOriginalCameraPhotos}
|
||||
/>
|
||||
);
|
||||
|
||||
const undeletedPhoto = screen.getByTestId(
|
||||
`PhotoCarousel.displayPhoto.${rotatedOriginalCameraPhotos[1]}`
|
||||
);
|
||||
expect( undeletedPhoto ).toBeVisible( );
|
||||
const deletedPhoto = screen.queryByTestId(
|
||||
`PhotoCarousel.displayPhoto.${rotatedOriginalCameraPhotos[2]}`
|
||||
);
|
||||
await waitFor( ( ) => {
|
||||
expect( deletedPhoto ).toBeFalsy( );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
82
tests/unit/components/Camera/StandardCamera.test.js
Normal file
82
tests/unit/components/Camera/StandardCamera.test.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
fireEvent, render, screen, waitFor
|
||||
} from "@testing-library/react-native";
|
||||
import StandardCamera from "components/Camera/StandardCamera/StandardCamera";
|
||||
import React from "react";
|
||||
import useStore from "stores/useStore";
|
||||
|
||||
jest.mock( "components/MediaViewer/MediaViewerModal", ( ) => jest.fn( ( ) => null ) );
|
||||
|
||||
const initialStoreState = useStore.getState( );
|
||||
|
||||
const mockPhotoUris = [
|
||||
"https://inaturalist-open-data.s3.amazonaws.com/photos/1/large.jpeg",
|
||||
"https://inaturalist-open-data.s3.amazonaws.com/photos/2/large.jpeg",
|
||||
"https://inaturalist-open-data.s3.amazonaws.com/photos/3/large.jpeg"
|
||||
];
|
||||
|
||||
describe( "StandardCamera", ( ) => {
|
||||
beforeAll( async () => {
|
||||
useStore.setState( initialStoreState, true );
|
||||
} );
|
||||
|
||||
it( "deletes a photo on long press", async ( ) => {
|
||||
const removePhotoFromList = ( list, photo ) => {
|
||||
const i = list.findIndex( p => p === photo );
|
||||
list.splice( i, 1 );
|
||||
return list || [];
|
||||
};
|
||||
|
||||
useStore.setState( {
|
||||
evidenceToAdd: [mockPhotoUris[2]],
|
||||
rotatedOriginalCameraPhotos: mockPhotoUris,
|
||||
deletePhotoFromObservation: uri => useStore.setState( {
|
||||
rotatedOriginalCameraPhotos: [...removePhotoFromList( mockPhotoUris, uri )]
|
||||
} )
|
||||
} );
|
||||
|
||||
const { rotatedOriginalCameraPhotos } = useStore.getState( );
|
||||
|
||||
render(
|
||||
<StandardCamera
|
||||
camera={{}}
|
||||
device={{}}
|
||||
/>
|
||||
|
||||
);
|
||||
const photoImage = screen.getByTestId(
|
||||
`PhotoCarousel.displayPhoto.${rotatedOriginalCameraPhotos[2]}`
|
||||
);
|
||||
const predeletedPhoto = screen.queryByTestId(
|
||||
`PhotoCarousel.displayPhoto.${rotatedOriginalCameraPhotos[2]}`
|
||||
);
|
||||
expect( predeletedPhoto ).toBeVisible( );
|
||||
|
||||
fireEvent( photoImage, "onLongPress" );
|
||||
const deleteMode = screen.getByTestId(
|
||||
`PhotoCarousel.deletePhoto.${rotatedOriginalCameraPhotos[2]}`
|
||||
);
|
||||
await waitFor( ( ) => {
|
||||
expect( deleteMode ).toBeVisible( );
|
||||
} );
|
||||
fireEvent.press( deleteMode );
|
||||
|
||||
render(
|
||||
<StandardCamera
|
||||
camera={{}}
|
||||
device={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
const undeletedPhoto = screen.getByTestId(
|
||||
`PhotoCarousel.displayPhoto.${rotatedOriginalCameraPhotos[1]}`
|
||||
);
|
||||
expect( undeletedPhoto ).toBeVisible( );
|
||||
const deletedPhoto = screen.queryByTestId(
|
||||
`PhotoCarousel.displayPhoto.${rotatedOriginalCameraPhotos[2]}`
|
||||
);
|
||||
await waitFor( ( ) => {
|
||||
expect( deletedPhoto ).toBeFalsy( );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -13,6 +13,7 @@ exports[`PhotoCarousel renders correctly 1`] = `
|
||||
}
|
||||
>
|
||||
<RCTScrollView
|
||||
ListEmptyComponent={null}
|
||||
data={
|
||||
[
|
||||
"https://inaturalist-open-data.s3.amazonaws.com/photos/1/large.jpeg",
|
||||
@@ -57,7 +58,7 @@ exports[`PhotoCarousel renders correctly 1`] = `
|
||||
"value": {
|
||||
"transform": [
|
||||
{
|
||||
"rotateZ": 0,
|
||||
"rotateZ": "0",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -67,7 +68,7 @@ exports[`PhotoCarousel renders correctly 1`] = `
|
||||
{
|
||||
"transform": [
|
||||
{
|
||||
"rotateZ": 0,
|
||||
"rotateZ": "0",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -226,7 +227,7 @@ exports[`PhotoCarousel renders correctly 1`] = `
|
||||
"value": {
|
||||
"transform": [
|
||||
{
|
||||
"rotateZ": 0,
|
||||
"rotateZ": "0",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -236,7 +237,7 @@ exports[`PhotoCarousel renders correctly 1`] = `
|
||||
{
|
||||
"transform": [
|
||||
{
|
||||
"rotateZ": 0,
|
||||
"rotateZ": "0",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -395,7 +396,7 @@ exports[`PhotoCarousel renders correctly 1`] = `
|
||||
"value": {
|
||||
"transform": [
|
||||
{
|
||||
"rotateZ": 0,
|
||||
"rotateZ": "0",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -405,7 +406,7 @@ exports[`PhotoCarousel renders correctly 1`] = `
|
||||
{
|
||||
"transform": [
|
||||
{
|
||||
"rotateZ": 0,
|
||||
"rotateZ": "0",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -563,6 +564,7 @@ exports[`PhotoCarousel renders correctly for large screen 1`] = `
|
||||
}
|
||||
>
|
||||
<RCTScrollView
|
||||
ListEmptyComponent={null}
|
||||
data={
|
||||
[
|
||||
"https://inaturalist-open-data.s3.amazonaws.com/photos/1/large.jpeg",
|
||||
@@ -607,7 +609,7 @@ exports[`PhotoCarousel renders correctly for large screen 1`] = `
|
||||
"value": {
|
||||
"transform": [
|
||||
{
|
||||
"rotateZ": 0,
|
||||
"rotateZ": "0",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -617,7 +619,7 @@ exports[`PhotoCarousel renders correctly for large screen 1`] = `
|
||||
{
|
||||
"transform": [
|
||||
{
|
||||
"rotateZ": 0,
|
||||
"rotateZ": "0",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -781,7 +783,7 @@ exports[`PhotoCarousel renders correctly for large screen 1`] = `
|
||||
"value": {
|
||||
"transform": [
|
||||
{
|
||||
"rotateZ": 0,
|
||||
"rotateZ": "0",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -791,7 +793,7 @@ exports[`PhotoCarousel renders correctly for large screen 1`] = `
|
||||
{
|
||||
"transform": [
|
||||
{
|
||||
"rotateZ": 0,
|
||||
"rotateZ": "0",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -955,7 +957,7 @@ exports[`PhotoCarousel renders correctly for large screen 1`] = `
|
||||
"value": {
|
||||
"transform": [
|
||||
{
|
||||
"rotateZ": 0,
|
||||
"rotateZ": "0",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -965,7 +967,7 @@ exports[`PhotoCarousel renders correctly for large screen 1`] = `
|
||||
{
|
||||
"transform": [
|
||||
{
|
||||
"rotateZ": 0,
|
||||
"rotateZ": "0",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user