From 1679f2f24ad549c63edabef3bbe2ee1989c0a6ea Mon Sep 17 00:00:00 2001 From: Ken-ichi Date: Wed, 31 Jul 2024 11:14:53 -0700 Subject: [PATCH] AI Camera gallery button (#1896) * Add button to import photos from the gallery to the AI Camera * Move AI Camera buttons to side rails so it's easier to add new buttons * Got shutter button closer to spec * Change e2e to test for element absence instead of status text Closes #1848 --- e2e/signedIn.e2e.js | 13 +-- .../Camera/AICamera/AICameraButtons.tsx | 83 +++++++++++-------- .../Camera/AICamera/AIDebugButton.js | 2 +- src/components/Camera/Buttons/Flash.tsx | 24 ++---- src/components/Camera/Buttons/Gallery.tsx | 48 +++++++++++ src/components/Camera/Buttons/TakePhoto.tsx | 10 +-- src/components/Camera/TabletButtons.tsx | 36 +++++--- src/components/PhotoImporter/PhotoGallery.js | 2 + 8 files changed, 143 insertions(+), 75 deletions(-) create mode 100644 src/components/Camera/Buttons/Gallery.tsx diff --git a/e2e/signedIn.e2e.js b/e2e/signedIn.e2e.js index 70f76e4c8..a8aa8cded 100644 --- a/e2e/signedIn.e2e.js +++ b/e2e/signedIn.e2e.js @@ -179,10 +179,13 @@ describe( "Signed in user", () => { / 5. Delete the observations */ await deleteObservationByUUID( uuid, username, { uploaded: true } ); - // TODO test to make sure the exact observation we created was deleted. - // Testing for the empty list UI isn't adequate because other test runs - // happening in parallel might cause other observations to be there - const deletedObservationText = element( by.text( /1 observation deleted/ ) ); - await waitFor( deletedObservationText ).toBeVisible().withTimeout( 10000 ); + // It would be nice to test for the "1 observation deleted" status text in + // the toolbar, but that message appears ephemerally and sometimes this + // test doesn't pick it up on the Github runner. Since we created two + // observations, the upload prompt will be the stable status text at this + // point, and we can confirm deletion by testing for the absence of the + // list item for the observation we deleted. + await waitFor( element( by.text( /Upload 1 observation/ ) ) ).toBeVisible( ).withTimeout( 10000 ); + await expect( obsListItem ).toBeNotVisible( ); } ); } ); diff --git a/src/components/Camera/AICamera/AICameraButtons.tsx b/src/components/Camera/AICamera/AICameraButtons.tsx index 765b52a53..9a3a095e2 100644 --- a/src/components/Camera/AICamera/AICameraButtons.tsx +++ b/src/components/Camera/AICamera/AICameraButtons.tsx @@ -1,6 +1,7 @@ import CameraFlip from "components/Camera/Buttons/CameraFlip.tsx"; import Close from "components/Camera/Buttons/Close.tsx"; import Flash from "components/Camera/Buttons/Flash.tsx"; +import Gallery from "components/Camera/Buttons/Gallery.tsx"; import TakePhoto from "components/Camera/Buttons/TakePhoto.tsx"; import Zoom from "components/Camera/Buttons/Zoom.tsx"; import TabletButtons from "components/Camera/TabletButtons.tsx"; @@ -67,6 +68,7 @@ const AICameraButtons = ( { disabled={!modelLoaded || takingPhoto} flipCamera={flipCamera} hasFlash={hasFlash} + hasGalleryButton rotatableAnimatedStyle={rotatableAnimatedStyle} showPrediction={showPrediction} showZoomButton={showZoomButton} @@ -78,48 +80,61 @@ const AICameraButtons = ( { ); } return ( - - - + + + - - - + + + + + + + + + + + - - + - ); diff --git a/src/components/Camera/AICamera/AIDebugButton.js b/src/components/Camera/AICamera/AIDebugButton.js index 4305ec469..d70527a74 100644 --- a/src/components/Camera/AICamera/AIDebugButton.js +++ b/src/components/Camera/AICamera/AIDebugButton.js @@ -66,7 +66,7 @@ const AIDebugButton = ( { if ( !isDebug ) return null; return ( - + void; - hasFlash: boolean; + hasFlash?: boolean; takePhotoOptions: TakePhotoOptions; flashClassName?: string; } -// Empty space where a camera button should be so buttons don't jump around -// when they appear or disappear -const CameraButtonPlaceholder = ( ) => ( - -); - const Flash = ( { rotatableAnimatedStyle, toggleFlash, @@ -39,7 +27,7 @@ const Flash = ( { }: Props ) => { const { t } = useTranslation( ); - if ( !hasFlash ) return ; + if ( !hasFlash ) return null; let testID = ""; let accessibilityHint = ""; let name = ""; @@ -59,9 +47,9 @@ const Flash = ( { { + const { t } = useTranslation( ); + const navigation = useNavigation( ); + + return ( + + navigation.push( "PhotoGallery", { cmonBack: true } )} + accessibilityLabel={t( "Photo-importer" )} + accessibilityHint={t( "Navigates-to-photo-importer" )} + icon="gallery" + color={( colors?.white as string ) || "white"} + size={26} + width={62} + height={62} + /> + + ); +}; + +export default Gallery; diff --git a/src/components/Camera/Buttons/TakePhoto.tsx b/src/components/Camera/Buttons/TakePhoto.tsx index 6eaf06090..e8961b3b2 100644 --- a/src/components/Camera/Buttons/TakePhoto.tsx +++ b/src/components/Camera/Buttons/TakePhoto.tsx @@ -11,7 +11,7 @@ import { useTranslation } from "sharedHooks"; import { getShadowForColor } from "styles/global"; import colors from "styles/tailwindColors"; -const DROP_SHADOW = getShadowForColor( colors.darkGray ); +const DROP_SHADOW = getShadowForColor( colors.black, { offsetHeight: 4 } ); interface Props { takePhoto: () => Promise; @@ -27,7 +27,7 @@ const TakePhoto = ( { const { t } = useTranslation( ); const theme = useTheme( ); - const borderClass = "border-[1.64px] rounded-full h-[60px] w-[60px]"; + const borderClass = "border-[2px] rounded-full h-[64px] w-[64px]"; return ( {showPrediction ? ( diff --git a/src/components/Camera/TabletButtons.tsx b/src/components/Camera/TabletButtons.tsx index 8058886ea..22a0bd13a 100644 --- a/src/components/Camera/TabletButtons.tsx +++ b/src/components/Camera/TabletButtons.tsx @@ -1,4 +1,5 @@ import classnames from "classnames"; +import Gallery from "components/Camera/Buttons/Gallery.tsx"; import { CloseButton } from "components/SharedComponents"; import { View } from "components/styledComponents"; import React from "react"; @@ -41,7 +42,8 @@ interface Props { flipCamera: ( _event: GestureResponderEvent ) => void; handleCheckmarkPress?: ( _event: GestureResponderEvent ) => void; handleClose?: ( _event: GestureResponderEvent ) => void; - hasFlash: boolean; + hasFlash?: boolean; + hasGalleryButton?: boolean; photosTaken?: boolean; rotatableAnimatedStyle: ViewStyle; showPrediction?: boolean; @@ -54,13 +56,15 @@ interface Props { // Empty space where a camera button should be so buttons don't jump around // when they appear or disappear -const CameraButtonPlaceholder = ( ) => ( +const CameraButtonPlaceholder = ( { extraClassName }: { extraClassName?: string } ) => ( ); @@ -72,6 +76,7 @@ const TabletButtons = ( { handleCheckmarkPress, handleClose, hasFlash, + hasGalleryButton, photosTaken, rotatableAnimatedStyle, showPrediction, @@ -83,33 +88,34 @@ const TabletButtons = ( { }: Props ) => { const tabletCameraOptionsClasses = [ "absolute", - "h-[380px]", "items-center", "justify-center", "mr-5", - "mt-[-190px]", - "pb-0", + "p-0", "right-0", - "top-[50%]" + "h-full" ]; return ( - + + { photosTaken && } null )} @@ -131,7 +137,7 @@ const TabletButtons = ( { - { !photosTaken && } + { hasFlash && } + { showZoomButton && } + { hasGalleryButton && ( + + + + ) } ); }; diff --git a/src/components/PhotoImporter/PhotoGallery.js b/src/components/PhotoImporter/PhotoGallery.js index e90f9a19c..b226861a0 100644 --- a/src/components/PhotoImporter/PhotoGallery.js +++ b/src/components/PhotoImporter/PhotoGallery.js @@ -123,6 +123,8 @@ const PhotoGallery = ( ): Node => { // Determine if we need to go back to ObsList or ObsDetails screen } else if ( params && params.previousScreen && params.previousScreen.name === "ObsDetails" ) { navigateToObsDetails( navigation, params.previousScreen.params.uuid ); + } else if ( params?.cmonBack && navigation.canGoBack() ) { + navigation.goBack(); } else { navToObsList(); }