mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-05-04 13:43:34 -04:00
Revert "Maintenance: break upload code into smaller modules -- realm syncing functions (#2872)"
This reverts commit 43f9520b86.
This commit is contained in:
@@ -48,7 +48,6 @@ type Props = {
|
||||
closeFiltersModal: Function,
|
||||
count: Object,
|
||||
currentExploreView: string,
|
||||
currentMapRegion: Object,
|
||||
filterByIconicTaxonUnknown: Function,
|
||||
handleUpdateCount: Function,
|
||||
hasLocationPermissions?: boolean,
|
||||
@@ -61,7 +60,6 @@ type Props = {
|
||||
renderLocationPermissionsGate: Function,
|
||||
requestLocationPermissions: Function,
|
||||
setCurrentExploreView: Function,
|
||||
setCurrentMapRegion: Function,
|
||||
showFiltersModal: boolean,
|
||||
startFetching: Function,
|
||||
updateLocation: Function,
|
||||
@@ -75,7 +73,6 @@ const Explore = ( {
|
||||
closeFiltersModal,
|
||||
count,
|
||||
currentExploreView,
|
||||
currentMapRegion,
|
||||
filterByIconicTaxonUnknown,
|
||||
handleUpdateCount,
|
||||
hasLocationPermissions,
|
||||
@@ -88,7 +85,6 @@ const Explore = ( {
|
||||
renderLocationPermissionsGate,
|
||||
requestLocationPermissions,
|
||||
setCurrentExploreView,
|
||||
setCurrentMapRegion,
|
||||
showFiltersModal,
|
||||
startFetching,
|
||||
updateLocation,
|
||||
@@ -162,8 +158,9 @@ const Explore = ( {
|
||||
layout={layout}
|
||||
queryParams={queryParams}
|
||||
handleUpdateCount={handleUpdateCount}
|
||||
currentMapRegion={currentMapRegion}
|
||||
setCurrentMapRegion={setCurrentMapRegion}
|
||||
hasLocationPermissions={hasLocationPermissions}
|
||||
renderLocationPermissionsGate={renderLocationPermissionsGate}
|
||||
requestLocationPermissions={requestLocationPermissions}
|
||||
/>
|
||||
)}
|
||||
{currentExploreView === "species" && (
|
||||
|
||||
@@ -26,8 +26,6 @@ const ExploreContainerWithContext = ( ): Node => {
|
||||
const { isConnected } = useNetInfo( );
|
||||
const exploreView = useStore( state => state.exploreView );
|
||||
const setExploreView = useStore( state => state.setExploreView );
|
||||
const mapRegion = useStore( s => s.mapRegion );
|
||||
const setMapRegion = useStore( s => s.setMapRegion );
|
||||
|
||||
const {
|
||||
hasPermissions: hasLocationPermissions,
|
||||
@@ -160,8 +158,6 @@ const ExploreContainerWithContext = ( ): Node => {
|
||||
renderLocationPermissionsGate={renderPermissionsGate}
|
||||
requestLocationPermissions={requestLocationPermissions}
|
||||
startFetching={startFetching}
|
||||
currentMapRegion={mapRegion}
|
||||
setCurrentMapRegion={setMapRegion}
|
||||
/>
|
||||
{renderPermissionsGate( {
|
||||
onPermissionGranted: startFetching
|
||||
|
||||
@@ -5,19 +5,48 @@ import {
|
||||
} from "components/SharedComponents";
|
||||
import { getMapRegion } from "components/SharedComponents/Map/helpers/mapHelpers.ts";
|
||||
import { View } from "components/styledComponents";
|
||||
import { MapBoundaries, PLACE_MODE, useExplore } from "providers/ExploreContext.tsx";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
EXPLORE_ACTION, MapBoundaries, PLACE_MODE, useExplore
|
||||
} from "providers/ExploreContext.tsx";
|
||||
import React, {
|
||||
useEffect, useMemo, useRef, useState
|
||||
} from "react";
|
||||
import { Region } from "react-native-maps";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
import { getShadow } from "styles/global";
|
||||
|
||||
import useMapLocation from "./hooks/useMapLocation";
|
||||
const NEARBY_DELTA = 0.02;
|
||||
|
||||
const WORLDWIDE_DELTA = 180;
|
||||
const WORLDWIDE_LAT_LNG = 0.0;
|
||||
|
||||
const worldwideRegion = {
|
||||
latitude: WORLDWIDE_LAT_LNG,
|
||||
longitude: WORLDWIDE_LAT_LNG,
|
||||
latitudeDelta: WORLDWIDE_DELTA,
|
||||
longitudeDelta: WORLDWIDE_DELTA
|
||||
};
|
||||
|
||||
const DROP_SHADOW = getShadow( {
|
||||
offsetHeight: 4,
|
||||
elevation: 6
|
||||
} );
|
||||
|
||||
const activityIndicatorSize = 50;
|
||||
const centeredLoadingWheel = {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: [
|
||||
{ translateX: -( activityIndicatorSize / 2 ) },
|
||||
{ translateY: -( activityIndicatorSize / 2 ) }
|
||||
],
|
||||
backgroundColor: "rgba(0,0,0,0)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 20
|
||||
};
|
||||
|
||||
interface Props {
|
||||
// Bounding box of the observations retrieved for the query params
|
||||
observationBounds?: MapBoundaries,
|
||||
@@ -27,42 +56,104 @@ interface Props {
|
||||
order?: string;
|
||||
orderBy?: string;
|
||||
};
|
||||
currentMapRegion: Region;
|
||||
setCurrentMapRegion: ( Region ) => void;
|
||||
isLoading: boolean
|
||||
isLoading: boolean,
|
||||
hasLocationPermissions?: boolean,
|
||||
renderLocationPermissionsGate: Function,
|
||||
requestLocationPermissions: Function
|
||||
}
|
||||
|
||||
const MapView = ( {
|
||||
observationBounds,
|
||||
queryParams,
|
||||
currentMapRegion,
|
||||
isLoading,
|
||||
setCurrentMapRegion
|
||||
hasLocationPermissions,
|
||||
renderLocationPermissionsGate,
|
||||
requestLocationPermissions
|
||||
}: Props ) => {
|
||||
const { t } = useTranslation( );
|
||||
const { state: exploreState } = useExplore( );
|
||||
const { state: exploreState, dispatch, defaultExploreLocation } = useExplore( );
|
||||
const [showRedoSearchButton, setShowRedoSearchButton] = useState( false );
|
||||
const isFirstRender = useRef( true );
|
||||
|
||||
const {
|
||||
onPanDrag,
|
||||
redoSearchInMapArea,
|
||||
region,
|
||||
showMapBoundaryButton,
|
||||
updateMapBoundaries
|
||||
} = useMapLocation( currentMapRegion, setCurrentMapRegion );
|
||||
const mapRef = useRef( null );
|
||||
|
||||
// TODO this should really be a part of the explore reducer
|
||||
useEffect( ( ) => {
|
||||
if (
|
||||
observationBounds
|
||||
&& [
|
||||
PLACE_MODE.WORLDWIDE,
|
||||
PLACE_MODE.PLACE,
|
||||
PLACE_MODE.NEARBY
|
||||
].indexOf( exploreState.placeMode ) >= 0
|
||||
) {
|
||||
updateMapBoundaries( getMapRegion( observationBounds ) );
|
||||
const nearbyRegion = useMemo( () => ( {
|
||||
latitude: exploreState.lat,
|
||||
longitude: exploreState.lng,
|
||||
latitudeDelta: NEARBY_DELTA,
|
||||
longitudeDelta: NEARBY_DELTA
|
||||
} ), [exploreState.lat, exploreState.lng] );
|
||||
|
||||
const regionFromCoordinates = useMemo( ( ) => {
|
||||
if ( exploreState.place?.point_geojson?.coordinates ) {
|
||||
const [longitude, latitude] = exploreState.place.point_geojson.coordinates;
|
||||
return {
|
||||
latitude,
|
||||
longitude,
|
||||
latitudeDelta: NEARBY_DELTA,
|
||||
longitudeDelta: NEARBY_DELTA
|
||||
};
|
||||
}
|
||||
}, [observationBounds, updateMapBoundaries, exploreState.placeMode] );
|
||||
return null;
|
||||
}, [exploreState.place] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
// Skip animation on first render
|
||||
if ( isFirstRender.current ) {
|
||||
isFirstRender.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( mapRef.current
|
||||
&& exploreState.placeMode === PLACE_MODE.MAP_AREA ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// since we're using initialRegion, we need to animate to the correct zoom level
|
||||
// when a user switches back to NEARBY or WORLDWIDE
|
||||
if ( mapRef.current
|
||||
&& exploreState.placeMode === PLACE_MODE.NEARBY ) {
|
||||
// Note: we do get observationBounds back from the API for nearby
|
||||
// but per user feedback, we want to show users a more zoomed in view
|
||||
// when they're looking at NEARBY view
|
||||
if ( nearbyRegion.latitude !== undefined && nearbyRegion.longitude !== undefined ) {
|
||||
mapRef.current.animateToRegion( nearbyRegion );
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ( mapRef.current
|
||||
&& exploreState.placeMode === PLACE_MODE.WORLDWIDE ) {
|
||||
mapRef.current.animateToRegion( worldwideRegion );
|
||||
}
|
||||
if ( mapRef.current
|
||||
&& exploreState.placeMode === PLACE_MODE.PLACE ) {
|
||||
if ( observationBounds ) {
|
||||
const newRegion = getMapRegion( observationBounds );
|
||||
mapRef.current.animateToRegion( newRegion );
|
||||
}
|
||||
}
|
||||
}, [
|
||||
exploreState.placeMode,
|
||||
nearbyRegion,
|
||||
regionFromCoordinates,
|
||||
observationBounds,
|
||||
exploreState.place?.id
|
||||
] );
|
||||
|
||||
const handleRedoSearch = async ( ) => {
|
||||
setShowRedoSearchButton( false );
|
||||
const currentBounds = await mapRef?.current?.getMapBoundaries( );
|
||||
dispatch( { type: EXPLORE_ACTION.SET_PLACE_MODE_MAP_AREA } );
|
||||
dispatch( {
|
||||
type: EXPLORE_ACTION.SET_MAP_BOUNDARIES,
|
||||
mapBoundaries: {
|
||||
swlat: currentBounds.southWest.latitude,
|
||||
swlng: currentBounds.southWest.longitude,
|
||||
nelat: currentBounds.northEast.latitude,
|
||||
nelng: currentBounds.northEast.longitude
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
const tileMapParams = {
|
||||
...queryParams
|
||||
@@ -72,10 +163,40 @@ const MapView = ( {
|
||||
delete tileMapParams.order;
|
||||
delete tileMapParams.orderBy;
|
||||
|
||||
const initialRegion: Region = useMemo( () => {
|
||||
if ( exploreState.placeMode === PLACE_MODE.NEARBY ) {
|
||||
if ( nearbyRegion.latitude !== undefined && nearbyRegion.longitude !== undefined ) {
|
||||
return nearbyRegion;
|
||||
}
|
||||
}
|
||||
|
||||
if ( exploreState.placeMode === PLACE_MODE.PLACE ) {
|
||||
if ( regionFromCoordinates ) {
|
||||
return regionFromCoordinates;
|
||||
}
|
||||
}
|
||||
|
||||
return worldwideRegion;
|
||||
}, [exploreState.placeMode, nearbyRegion, regionFromCoordinates] );
|
||||
|
||||
const handlePanDrag = ( ) => setShowRedoSearchButton( true );
|
||||
|
||||
const handleCurrentLocationPress = async ( ) => {
|
||||
if ( hasLocationPermissions ) {
|
||||
const exploreLocation = await defaultExploreLocation( );
|
||||
dispatch( {
|
||||
type: EXPLORE_ACTION.SET_EXPLORE_LOCATION,
|
||||
exploreLocation
|
||||
} );
|
||||
} else {
|
||||
requestLocationPermissions( );
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-1 overflow-hidden h-full">
|
||||
<View className="z-10">
|
||||
{showMapBoundaryButton && (
|
||||
{showRedoSearchButton && (
|
||||
<View
|
||||
className="mx-auto"
|
||||
style={DROP_SHADOW}
|
||||
@@ -84,28 +205,31 @@ const MapView = ( {
|
||||
text={t( "REDO-SEARCH-IN-MAP-AREA" )}
|
||||
level="focus"
|
||||
className="top-[60px] absolute self-center"
|
||||
onPress={redoSearchInMapArea}
|
||||
onPress={handleRedoSearch}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{isLoading
|
||||
? <View className="h-full flex justify-center"><ActivityIndicator size={50} /></View>
|
||||
: (
|
||||
<Map
|
||||
currentLocationButtonClassName="left-5 bottom-20"
|
||||
onPanDrag={onPanDrag}
|
||||
onRegionChangeComplete={updateMapBoundaries}
|
||||
region={region}
|
||||
showCurrentLocationButton
|
||||
showSwitchMapTypeButton
|
||||
showsCompass={false}
|
||||
switchMapTypeButtonClassName="left-20 bottom-20"
|
||||
showsUserLocation
|
||||
tileMapParams={tileMapParams}
|
||||
withPressableObsTiles={tileMapParams !== null}
|
||||
/>
|
||||
)}
|
||||
<Map
|
||||
ref={mapRef}
|
||||
currentLocationButtonClassName="left-5 bottom-20"
|
||||
onPanDrag={handlePanDrag}
|
||||
initialRegion={initialRegion}
|
||||
showCurrentLocationButton
|
||||
showSwitchMapTypeButton
|
||||
showsCompass={false}
|
||||
switchMapTypeButtonClassName="left-20 bottom-20"
|
||||
showsUserLocation
|
||||
tileMapParams={tileMapParams}
|
||||
withPressableObsTiles={tileMapParams !== null}
|
||||
onCurrentLocationPress={handleCurrentLocationPress}
|
||||
/>
|
||||
{isLoading && (
|
||||
<View style={centeredLoadingWheel} testID="activity-indicator">
|
||||
<ActivityIndicator size={activityIndicatorSize} />
|
||||
</View>
|
||||
)}
|
||||
{renderLocationPermissionsGate( { onPermissionGranted: handleCurrentLocationPress } )}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,8 +28,9 @@ type Props = {
|
||||
layout: string,
|
||||
queryParams: Object,
|
||||
handleUpdateCount: Function,
|
||||
currentMapRegion: Object,
|
||||
setCurrentMapRegion: Function
|
||||
hasLocationPermissions?: boolean,
|
||||
renderLocationPermissionsGate: Function,
|
||||
requestLocationPermissions: Function
|
||||
}
|
||||
|
||||
const OBS_LIST_CONTAINER_STYLE = { paddingTop: 50 };
|
||||
@@ -41,8 +42,9 @@ const ObservationsView = ( {
|
||||
layout,
|
||||
queryParams,
|
||||
handleUpdateCount,
|
||||
currentMapRegion,
|
||||
setCurrentMapRegion
|
||||
hasLocationPermissions,
|
||||
renderLocationPermissionsGate,
|
||||
requestLocationPermissions
|
||||
}: Props ): Node => {
|
||||
const currentUser = useCurrentUser( );
|
||||
const { state } = useExplore();
|
||||
@@ -139,8 +141,9 @@ const ObservationsView = ( {
|
||||
observationBounds={totalBounds}
|
||||
isLoading={isLoading}
|
||||
queryParams={queryParams}
|
||||
currentMapRegion={currentMapRegion}
|
||||
setCurrentMapRegion={setCurrentMapRegion}
|
||||
hasLocationPermissions={hasLocationPermissions}
|
||||
renderLocationPermissionsGate={renderLocationPermissionsGate}
|
||||
requestLocationPermissions={requestLocationPermissions}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ import { getShadow } from "styles/global";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
type Props = {
|
||||
gridFirst?: boolean,
|
||||
hideMap?: boolean,
|
||||
layout: string,
|
||||
updateObservationsView: Function
|
||||
@@ -20,30 +19,26 @@ const DROP_SHADOW = getShadow( {
|
||||
} );
|
||||
|
||||
const ObservationsViewBar = ( {
|
||||
gridFirst,
|
||||
hideMap,
|
||||
layout,
|
||||
updateObservationsView
|
||||
}: Props ): Node => {
|
||||
const buttons = [
|
||||
{
|
||||
value: "list",
|
||||
icon: "hamburger-menu",
|
||||
accessibilityLabel: "List",
|
||||
testID: "SegmentedButton.list"
|
||||
},
|
||||
{
|
||||
value: "grid",
|
||||
icon: "grid",
|
||||
accessibilityLabel: "Grid",
|
||||
testID: "SegmentedButton.grid"
|
||||
},
|
||||
{
|
||||
value: "list",
|
||||
icon: "hamburger-menu",
|
||||
accessibilityLabel: "List",
|
||||
testID: "SegmentedButton.list"
|
||||
}
|
||||
];
|
||||
if ( gridFirst ) {
|
||||
buttons.reverse( );
|
||||
}
|
||||
if ( !hideMap ) {
|
||||
buttons.push( {
|
||||
buttons.unshift( {
|
||||
value: "map",
|
||||
icon: "map",
|
||||
accessibilityLabel: "Map",
|
||||
|
||||
@@ -33,8 +33,6 @@ const RootExploreContainerWithContext = ( ): Node => {
|
||||
const setRootExploreView = useStore( state => state.setRootExploreView );
|
||||
const rootStoredParams = useStore( state => state.rootStoredParams );
|
||||
const setRootStoredParams = useStore( state => state.setRootStoredParams );
|
||||
const rootMapRegion = useStore( s => s.rootMapRegion );
|
||||
const setRootMapRegion = useStore( s => s.setRootMapRegion );
|
||||
|
||||
const {
|
||||
hasPermissions: hasLocationPermissions,
|
||||
@@ -231,8 +229,6 @@ const RootExploreContainerWithContext = ( ): Node => {
|
||||
hasLocationPermissions={hasLocationPermissions}
|
||||
requestLocationPermissions={requestLocationPermissions}
|
||||
startFetching={startFetching}
|
||||
currentMapRegion={rootMapRegion}
|
||||
setCurrentMapRegion={setRootMapRegion}
|
||||
renderLocationPermissionsGate={renderPermissionsGate}
|
||||
/>
|
||||
{renderPermissionsGate( {
|
||||
|
||||
@@ -52,6 +52,7 @@ const ExploreTaxonSearch = ( {
|
||||
const renderItem = useCallback( ( { item: taxon, index } ) => (
|
||||
<TaxonResult
|
||||
first={index === 0}
|
||||
fetchRemote={false}
|
||||
handleTaxonOrEditPress={() => onTaxonSelected( taxon )}
|
||||
hideInfoButton={hideInfoButton}
|
||||
onPressInfo={onPressInfo}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import {
|
||||
EXPLORE_ACTION,
|
||||
MapBoundaries,
|
||||
PLACE_MODE,
|
||||
useExplore
|
||||
} from "providers/ExploreContext.tsx";
|
||||
import {
|
||||
useCallback, useEffect, useRef, useState
|
||||
} from "react";
|
||||
import { BoundingBox, Region } from "react-native-maps";
|
||||
import { initialMapRegion } from "stores/createExploreSlice.ts";
|
||||
|
||||
const useMapLocation = ( currentMapRegion, setCurrentMapRegion ) => {
|
||||
const { dispatch, state } = useExplore( );
|
||||
const [mapBoundaries, setMapBoundaries] = useState<MapBoundaries>( );
|
||||
const [showMapBoundaryButton, setShowMapBoundaryButton] = useState( false );
|
||||
|
||||
const place = state?.place;
|
||||
|
||||
const onPanDrag = ( ) => setShowMapBoundaryButton( true );
|
||||
|
||||
const mapWasReset = state.placeMode === PLACE_MODE.NEARBY
|
||||
|| state.placeMode === PLACE_MODE.WORLDWIDE;
|
||||
const placeIdWasSet = state.place_id;
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const updateMapBoundaries = useCallback( async ( newRegion: Region, boundaries?: BoundingBox ) => {
|
||||
setCurrentMapRegion( newRegion );
|
||||
if ( boundaries ) {
|
||||
const newMapBoundaries = {
|
||||
swlat: boundaries.southWest.latitude,
|
||||
swlng: boundaries.southWest.longitude,
|
||||
nelat: boundaries.northEast.latitude,
|
||||
nelng: boundaries.northEast.longitude
|
||||
};
|
||||
setMapBoundaries( newMapBoundaries );
|
||||
return newMapBoundaries;
|
||||
}
|
||||
return {};
|
||||
}, [
|
||||
setMapBoundaries,
|
||||
setCurrentMapRegion
|
||||
] );
|
||||
|
||||
const redoSearchInMapArea = ( ) => {
|
||||
if ( !mapBoundaries ) return;
|
||||
setShowMapBoundaryButton( false );
|
||||
dispatch( { type: EXPLORE_ACTION.SET_PLACE_MODE_MAP_AREA } );
|
||||
dispatch( { type: EXPLORE_ACTION.SET_MAP_BOUNDARIES, mapBoundaries } );
|
||||
};
|
||||
|
||||
const previousPlaceGuess = useRef( state.placeMode );
|
||||
useEffect( ( ) => {
|
||||
// region gets set when a user is navigating from ExploreLocationSearch
|
||||
if ( placeIdWasSet ) {
|
||||
const coordinates = place?.point_geojson?.coordinates
|
||||
? place.point_geojson.coordinates
|
||||
: place?.bounding_box_geojson?.coordinates;
|
||||
if ( coordinates ) {
|
||||
setCurrentMapRegion( {
|
||||
...initialMapRegion,
|
||||
latitude: coordinates[1],
|
||||
longitude: coordinates[0]
|
||||
} );
|
||||
}
|
||||
} else if ( mapWasReset ) {
|
||||
// map gets set or reset back to nearby/worldwide, but only if the placeMode
|
||||
// has changed
|
||||
if ( previousPlaceGuess.current === state.placeMode ) {
|
||||
return;
|
||||
}
|
||||
setCurrentMapRegion( {
|
||||
...initialMapRegion,
|
||||
latitude: state?.lat,
|
||||
longitude: state?.lng
|
||||
} );
|
||||
previousPlaceGuess.current = state.placeMode;
|
||||
}
|
||||
}, [
|
||||
mapWasReset,
|
||||
place,
|
||||
placeIdWasSet,
|
||||
setCurrentMapRegion,
|
||||
state
|
||||
] );
|
||||
|
||||
return {
|
||||
onPanDrag,
|
||||
redoSearchInMapArea,
|
||||
region: currentMapRegion,
|
||||
showMapBoundaryButton,
|
||||
updateMapBoundaries
|
||||
};
|
||||
};
|
||||
|
||||
export default useMapLocation;
|
||||
@@ -309,7 +309,6 @@ const MyObservationsSimple = ( {
|
||||
: <Announcements isConnected={isConnected} /> )}
|
||||
/>
|
||||
<ObservationsViewBar
|
||||
gridFirst
|
||||
hideMap
|
||||
layout={layout}
|
||||
updateObservationsView={toggleLayout}
|
||||
|
||||
@@ -20,6 +20,7 @@ import React, {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Image,
|
||||
Platform,
|
||||
StatusBar,
|
||||
useWindowDimensions,
|
||||
View
|
||||
@@ -163,7 +164,12 @@ const OnboardingCarousel = ( ) => {
|
||||
} );
|
||||
}, [ONBOARDING_SLIDES, totalImages] );
|
||||
|
||||
if ( !imagesLoaded ) {
|
||||
// TODO: On Android release build imagesLoaded never switched from false to true, and
|
||||
// this screen was stuck in a loading state. On iOS it worked as expected.
|
||||
// Disabling it now on Android to make a new release possible.
|
||||
if ( Platform.OS === "android"
|
||||
? false
|
||||
: !imagesLoaded ) {
|
||||
return (
|
||||
<ImageBackground
|
||||
source={require( "images/background/daniel-olah-YNUFtf4qyh0-unsplash.jpg" )}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { tailwindFontBold } from "appConstants/fontFamilies.ts";
|
||||
import classnames from "classnames";
|
||||
import { ActivityIndicator, Heading4, INatIcon } from "components/SharedComponents";
|
||||
import { Pressable, View } from "components/styledComponents";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { AccessibilityRole, GestureResponderEvent, ViewStyle } from "react-native";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
@@ -181,9 +181,7 @@ const Button = ( {
|
||||
const [isProcessing, setIsProcessing] = useState( false );
|
||||
const onPressRef = useRef( onPress );
|
||||
|
||||
useEffect( ( ) => {
|
||||
onPressRef.current = onPress;
|
||||
}, [onPress] );
|
||||
onPressRef.current = onPress;
|
||||
|
||||
const isPrimary = level === "primary";
|
||||
const isWarning = level === "warning";
|
||||
@@ -210,19 +208,16 @@ const Button = ( {
|
||||
|
||||
const handlePress = ( event?: GestureResponderEvent ) => {
|
||||
if ( !preventMultipleTaps ) {
|
||||
onPressRef.current( event );
|
||||
return;
|
||||
return onPressRef.current( event );
|
||||
}
|
||||
|
||||
if ( isProcessing ) return;
|
||||
|
||||
setIsProcessing( true );
|
||||
|
||||
onPressRef.current( event );
|
||||
|
||||
setTimeout( ( ) => {
|
||||
setIsProcessing( false );
|
||||
}, debounceTime );
|
||||
return null;
|
||||
};
|
||||
|
||||
const isDisabled = disabled || ( preventMultipleTaps && isProcessing );
|
||||
|
||||
@@ -31,6 +31,7 @@ const CurrentLocationButton = ( {
|
||||
style={DROP_SHADOW}
|
||||
accessibilityLabel={t( "Zoom-to-current-location" )}
|
||||
onPress={onPress}
|
||||
testID="Map.CurrentLocationButton"
|
||||
/>
|
||||
{renderPermissionsGate( )}
|
||||
</>
|
||||
|
||||
@@ -3,6 +3,7 @@ import classnames from "classnames";
|
||||
import { Body1 } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -53,7 +54,7 @@ interface Props {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
currentLocationButtonClassName?: string;
|
||||
initialRegion?: boolean;
|
||||
initialRegion?: Region;
|
||||
mapHeight?: DimensionValue; // allows for height to be defined as px or percentage
|
||||
mapType?: MapType;
|
||||
mapViewClassName?: string;
|
||||
@@ -64,7 +65,7 @@ interface Props {
|
||||
onRegionChangeComplete?: ( _r: Region, _b: BoundingBox | undefined ) => void;
|
||||
openMapScreen?: () => void;
|
||||
region?: Region;
|
||||
regionToAnimate?: Object;
|
||||
regionToAnimate?: Region;
|
||||
scrollEnabled?: boolean;
|
||||
showCurrentLocationButton?: boolean;
|
||||
showsCompass?: boolean;
|
||||
@@ -82,7 +83,7 @@ interface Props {
|
||||
|
||||
// TODO: fallback to another map library
|
||||
// for people who don't use GMaps (i.e. users in China)
|
||||
const Map = ( {
|
||||
const Map = forwardRef( ( {
|
||||
children,
|
||||
className = "flex-1",
|
||||
currentLocationButtonClassName,
|
||||
@@ -111,7 +112,7 @@ const Map = ( {
|
||||
withPressableObsTiles,
|
||||
zoomEnabled = true,
|
||||
zoomTapEnabled = true
|
||||
}: Props ) => {
|
||||
}: Props, ref ) => {
|
||||
const { isDebug } = useDebugMode( );
|
||||
const { screenWidth, screenHeight } = useDeviceOrientation( );
|
||||
const [currentZoom, setCurrentZoom] = useState( 0 );
|
||||
@@ -243,7 +244,9 @@ const Map = ( {
|
||||
// If we're supposed to be showing user location but we don't have it, ask
|
||||
// for permission again, which should result in fetching the location if
|
||||
// we can
|
||||
if ( !userLocation ) {
|
||||
// skipping onCurrentLocationPress here because the handlers
|
||||
// are handling the permissions request outside of this component (example: Explore MapView)
|
||||
if ( !userLocation && onCurrentLocationPress === undefined ) {
|
||||
requestPermissions( );
|
||||
return;
|
||||
}
|
||||
@@ -413,9 +416,9 @@ const Map = ( {
|
||||
: unfuzzedMapRegion;
|
||||
|
||||
// In Android, we maintain initialRegion as state localRegion and
|
||||
// pass null to parameter initialRegion.
|
||||
// pass undefined to parameter initialRegion.
|
||||
const mapInitialRegion = Platform.OS === "android"
|
||||
? null
|
||||
? undefined
|
||||
: initialRegion;
|
||||
|
||||
const renderDebugZoomLevel = ( ) => {
|
||||
@@ -448,6 +451,18 @@ const Map = ( {
|
||||
const longitude = observation?.privateLongitude || observation?.longitude;
|
||||
const hasCoordinates = latitude && longitude;
|
||||
|
||||
const setRefs = instance => {
|
||||
// Update our internal ref
|
||||
mapViewRef.current = instance;
|
||||
|
||||
// Forward to the parent ref
|
||||
if ( typeof ref === "function" ) {
|
||||
ref( instance );
|
||||
} else if ( ref ) {
|
||||
ref.current = instance;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={mapContainerStyle}
|
||||
@@ -469,7 +484,7 @@ const Map = ( {
|
||||
onRegionChangeComplete={handleRegionChangeComplete}
|
||||
onUserLocationChange={handleUserLocationChange}
|
||||
pitchEnabled={false}
|
||||
ref={mapViewRef}
|
||||
ref={setRefs}
|
||||
region={mapRegion}
|
||||
rotateEnabled={false}
|
||||
scrollEnabled={scrollEnabled}
|
||||
@@ -529,6 +544,6 @@ const Map = ( {
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
} );
|
||||
|
||||
export default Map;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { fontRegular } from "appConstants/fontFamilies.ts";
|
||||
import classNames from "classnames";
|
||||
import { INatIcon, INatIconButton } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import React from "react";
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import { Keyboard, TextInput as RNTextInput } from "react-native";
|
||||
import { TextInput, useTheme } from "react-native-paper";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
@@ -21,6 +21,7 @@ interface Props {
|
||||
placeholder?: string;
|
||||
testID?: string;
|
||||
value: string;
|
||||
debounceTime?: number;
|
||||
}
|
||||
|
||||
// Ensure this component is placed outside of scroll views
|
||||
@@ -34,10 +35,26 @@ const SearchBar = ( {
|
||||
input,
|
||||
placeholder,
|
||||
testID,
|
||||
value
|
||||
value,
|
||||
debounceTime = 300
|
||||
}: Props ) => {
|
||||
const theme = useTheme( );
|
||||
const { t } = useTranslation( );
|
||||
const [localValue, setLocalValue] = useState( value );
|
||||
|
||||
const debounceTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const debouncedHandleTextChange = useCallback( ( text: string ) => {
|
||||
setLocalValue( text );
|
||||
|
||||
if ( debounceTimeout.current ) {
|
||||
clearTimeout( debounceTimeout.current );
|
||||
}
|
||||
|
||||
debounceTimeout.current = setTimeout( ( ) => {
|
||||
handleTextChange( text );
|
||||
}, debounceTime );
|
||||
}, [handleTextChange, debounceTime] );
|
||||
|
||||
const outlineStyle = {
|
||||
borderColor: "lightgray",
|
||||
@@ -76,7 +93,7 @@ const SearchBar = ( {
|
||||
dense
|
||||
keyboardType="default"
|
||||
mode="outlined"
|
||||
onChangeText={handleTextChange}
|
||||
onChangeText={debouncedHandleTextChange}
|
||||
outlineStyle={outlineStyle}
|
||||
placeholder={placeholder}
|
||||
selectionColor={colors.darkGray}
|
||||
@@ -84,9 +101,9 @@ const SearchBar = ( {
|
||||
testID={testID}
|
||||
theme={fontTheme}
|
||||
underlineColor={colors.darkGray}
|
||||
value={value}
|
||||
value={localValue}
|
||||
/>
|
||||
{value?.length > 0 && clearSearch
|
||||
{localValue?.length > 0 && clearSearch
|
||||
? (
|
||||
<View className="absolute right-0">
|
||||
<INatIconButton
|
||||
@@ -96,6 +113,7 @@ const SearchBar = ( {
|
||||
onPress={() => {
|
||||
Keyboard.dismiss();
|
||||
clearSearch();
|
||||
setLocalValue( "" );
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { fetchSearchResults } from "api/search.ts";
|
||||
import type { ApiOpts } from "api/types";
|
||||
import { RealmContext } from "providers/contexts.ts";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
useCallback, useEffect, useMemo, useState
|
||||
} from "react";
|
||||
import Realm, { UpdateMode } from "realm";
|
||||
import Taxon from "realmModels/Taxon";
|
||||
import type { RealmTaxon } from "realmModels/types";
|
||||
@@ -34,6 +36,8 @@ const useTaxonSearch = ( taxonQueryArg = "" ) => {
|
||||
const taxonQuery = taxonQueryArg.trim();
|
||||
const [localTaxa, setLocalTaxa] = useState<RealmTaxon[] | null>( null );
|
||||
|
||||
const shouldFetchRemote = taxonQuery.length > 0;
|
||||
|
||||
const { data: remoteTaxa, refetch, isLoading } = useAuthenticatedQuery(
|
||||
["fetchTaxonSuggestions", taxonQuery],
|
||||
async ( optsWithAuth: ApiOpts ) => {
|
||||
@@ -50,16 +54,10 @@ const useTaxonSearch = ( taxonQueryArg = "" ) => {
|
||||
return apiTaxa?.map( taxon => Taxon.mapApiToRealm( taxon ) ) || [];
|
||||
},
|
||||
{
|
||||
enabled: !!( taxonQuery.length > 0 )
|
||||
enabled: shouldFetchRemote
|
||||
}
|
||||
);
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( realm && remoteTaxa?.length > 0 ) {
|
||||
saveTaxaToRealm( remoteTaxa, realm );
|
||||
}
|
||||
}, [realm, remoteTaxa] );
|
||||
|
||||
const safeRealmSearch = useCallback( async ( searchString: string ) => {
|
||||
try {
|
||||
const { cleanedQuery } = validateRealmSearch( searchString );
|
||||
@@ -82,53 +80,89 @@ const useTaxonSearch = ( taxonQueryArg = "" ) => {
|
||||
}, [realm] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
const searchLocalTaxa = async ( ) => {
|
||||
if (
|
||||
taxonQuery.length > 0
|
||||
&& !isLoading
|
||||
&& ( !remoteTaxa || remoteTaxa.length === 0 )
|
||||
) {
|
||||
try {
|
||||
const results = await safeRealmSearch( taxonQuery );
|
||||
setLocalTaxa( results );
|
||||
} catch ( error ) {
|
||||
console.error( "Local search failed:", error );
|
||||
setLocalTaxa( [] );
|
||||
}
|
||||
} else {
|
||||
setLocalTaxa( null );
|
||||
let isSubscribed = true;
|
||||
const saveOrSearchRealmTaxa = async ( ) => {
|
||||
// save taxa to realm if we have results from the API
|
||||
if ( realm && remoteTaxa?.length > 0 ) {
|
||||
saveTaxaToRealm( remoteTaxa, realm );
|
||||
}
|
||||
// Search for local taxa if we have a query, if remote results are not loading
|
||||
// and if remote results are empty
|
||||
if ( taxonQuery.length === 0 ) {
|
||||
if ( isSubscribed ) setLocalTaxa( null );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isLoading ) return;
|
||||
|
||||
if ( remoteTaxa && remoteTaxa.length > 0 ) {
|
||||
if ( isSubscribed ) setLocalTaxa( null );
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await safeRealmSearch( taxonQuery );
|
||||
if ( isSubscribed ) setLocalTaxa( results );
|
||||
} catch ( error ) {
|
||||
console.error( "Local search failed:", error );
|
||||
if ( isSubscribed ) setLocalTaxa( [] );
|
||||
}
|
||||
};
|
||||
|
||||
searchLocalTaxa( );
|
||||
}, [taxonQuery, isLoading, remoteTaxa, safeRealmSearch] );
|
||||
saveOrSearchRealmTaxa( );
|
||||
|
||||
// Show iconic taxa by default
|
||||
if ( taxonQuery.length === 0 ) {
|
||||
return ( ) => {
|
||||
isSubscribed = false;
|
||||
};
|
||||
}, [
|
||||
isLoading,
|
||||
realm,
|
||||
remoteTaxa,
|
||||
safeRealmSearch,
|
||||
taxonQuery
|
||||
] );
|
||||
|
||||
return useMemo( () => {
|
||||
// Show iconic taxa by default (empty query)
|
||||
if ( taxonQuery.length === 0 ) {
|
||||
return {
|
||||
taxa: iconicTaxa,
|
||||
refetch: () => undefined,
|
||||
isLoading: false,
|
||||
isLocal: false
|
||||
};
|
||||
}
|
||||
|
||||
// Show remote taxa if available
|
||||
if ( remoteTaxa && remoteTaxa.length > 0 ) {
|
||||
return {
|
||||
taxa: remoteTaxa,
|
||||
refetch,
|
||||
isLoading,
|
||||
isLocal: false
|
||||
};
|
||||
}
|
||||
|
||||
// Show local taxa if available
|
||||
if ( localTaxa !== null && localTaxa.length > 0 ) {
|
||||
return {
|
||||
taxa: localTaxa,
|
||||
refetch: () => undefined,
|
||||
isLoading: false,
|
||||
isLocal: true
|
||||
};
|
||||
}
|
||||
|
||||
// Still loading or no results
|
||||
return {
|
||||
taxa: iconicTaxa,
|
||||
refetch: ( ) => undefined,
|
||||
isLoading: false,
|
||||
taxa: isLoading
|
||||
? []
|
||||
: localTaxa || [],
|
||||
refetch,
|
||||
isLoading,
|
||||
isLocal: false
|
||||
};
|
||||
}
|
||||
|
||||
// Show local taxa if available
|
||||
if ( localTaxa !== null && localTaxa.length > 0 ) {
|
||||
return {
|
||||
taxa: localTaxa,
|
||||
refetch: ( ) => undefined,
|
||||
isLoading: false,
|
||||
isLocal: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
taxa: remoteTaxa,
|
||||
refetch,
|
||||
isLoading,
|
||||
isLocal: false
|
||||
};
|
||||
}, [taxonQuery, remoteTaxa, localTaxa, iconicTaxa, refetch, isLoading] );
|
||||
};
|
||||
|
||||
export default useTaxonSearch;
|
||||
|
||||
@@ -1,37 +1,17 @@
|
||||
import { StateCreator } from "zustand";
|
||||
|
||||
const DELTA = 0.2;
|
||||
|
||||
export const initialMapRegion = {
|
||||
latitude: 0.0,
|
||||
longitude: 0.0,
|
||||
latitudeDelta: DELTA,
|
||||
longitudeDelta: DELTA
|
||||
};
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
exploreView: "species",
|
||||
mapRegion: initialMapRegion
|
||||
exploreView: "observations"
|
||||
};
|
||||
|
||||
interface MapRegion {
|
||||
latitude: number,
|
||||
longitude: number,
|
||||
latitudeDelta: number,
|
||||
longitudeDelta: number
|
||||
}
|
||||
|
||||
interface ExploreSlice {
|
||||
exploreView: string,
|
||||
setExploreView: ( _view: string ) => void,
|
||||
mapRegion: MapRegion,
|
||||
setMapRegion: ( _region: MapRegion ) => void
|
||||
setExploreView: ( _view: string ) => void
|
||||
}
|
||||
|
||||
const createExploreSlice: StateCreator<ExploreSlice> = set => ( {
|
||||
...DEFAULT_STATE,
|
||||
setExploreView: exploreView => set( ( ) => ( { exploreView } ) ),
|
||||
setMapRegion: mapRegion => set( ( ) => ( { mapRegion } ) )
|
||||
setExploreView: exploreView => set( ( ) => ( { exploreView } ) )
|
||||
} );
|
||||
|
||||
export default createExploreSlice;
|
||||
|
||||
@@ -1,41 +1,21 @@
|
||||
import { StateCreator } from "zustand";
|
||||
|
||||
const DELTA = 0.2;
|
||||
|
||||
export const initialMapRegion = {
|
||||
latitude: 0.0,
|
||||
longitude: 0.0,
|
||||
latitudeDelta: DELTA,
|
||||
longitudeDelta: DELTA
|
||||
};
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
rootStoredParams: {},
|
||||
rootExploreView: "species",
|
||||
rootMapRegion: initialMapRegion
|
||||
rootExploreView: "observations"
|
||||
};
|
||||
|
||||
interface MapRegion {
|
||||
latitude: number,
|
||||
longitude: number,
|
||||
latitudeDelta: number,
|
||||
longitudeDelta: number
|
||||
}
|
||||
|
||||
interface RootExploreSlice {
|
||||
rootStoredParams: Object,
|
||||
setRootStoredParams: ( _params: Object ) => void,
|
||||
rootExploreView: string,
|
||||
setRootExploreView: ( _view: string ) => void,
|
||||
rootMapRegion: MapRegion,
|
||||
setRootMapRegion: ( _region: MapRegion ) => void
|
||||
setRootExploreView: ( _view: string ) => void
|
||||
}
|
||||
|
||||
const createRootExploreSlice: StateCreator<RootExploreSlice> = set => ( {
|
||||
...DEFAULT_STATE,
|
||||
setRootStoredParams: rootStoredParams => set( ( ) => ( { rootStoredParams } ) ),
|
||||
setRootExploreView: rootExploreView => set( ( ) => ( { rootExploreView } ) ),
|
||||
setRootMapRegion: rootMapRegion => set( ( ) => ( { rootMapRegion } ) )
|
||||
setRootExploreView: rootExploreView => set( ( ) => ( { rootExploreView } ) )
|
||||
} );
|
||||
|
||||
export default createRootExploreSlice;
|
||||
|
||||
@@ -2,8 +2,7 @@ import {
|
||||
fireEvent,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
within
|
||||
waitFor
|
||||
} from "@testing-library/react-native";
|
||||
import ExploreContainer from "components/Explore/ExploreContainer";
|
||||
import inatjs from "inaturalistjs";
|
||||
@@ -36,24 +35,25 @@ beforeAll( ( ) => {
|
||||
jest.useFakeTimers( );
|
||||
} );
|
||||
|
||||
const switchToObservationsView = async ( ) => {
|
||||
const switchToSpeciesView = async ( ) => {
|
||||
const observationsViewIcon = await screen.findByLabelText( /Observations View/ );
|
||||
expect( observationsViewIcon ).toBeVisible( );
|
||||
await actor.press( observationsViewIcon );
|
||||
const speciesRadioButton = await screen.findByText( "Species" );
|
||||
await actor.press( speciesRadioButton );
|
||||
const confirmButton = await screen.findByText( /EXPLORE SPECIES/ );
|
||||
await actor.press( confirmButton );
|
||||
const speciesViewIcon = await screen.findByLabelText( /Species View/ );
|
||||
expect( speciesViewIcon ).toBeVisible( );
|
||||
await actor.press( speciesViewIcon );
|
||||
const observationsRadioButton = await screen.findByText( "Observations" );
|
||||
await actor.press( observationsRadioButton );
|
||||
const bottomSheet = await screen.findByTestId( "ExploreObsViewSheet" );
|
||||
const confirmButton = await within( bottomSheet ).findByText( /EXPLORE OBSERVATIONS/ );
|
||||
expect( confirmButton ).toBeVisible( );
|
||||
await actor.press( confirmButton );
|
||||
const obsTaxonNameElt = await screen.findByText( mockRemoteObservation.taxon.name );
|
||||
expect( obsTaxonNameElt ).toBeTruthy( );
|
||||
};
|
||||
|
||||
describe( "Explore", ( ) => {
|
||||
it( "should render species view and switch to observations view list correctly", async ( ) => {
|
||||
it( "should render observations view list correctly on page load", async ( ) => {
|
||||
renderAppWithComponent( <ExploreContainer /> );
|
||||
await switchToObservationsView( );
|
||||
const observationsViewIcon = await screen.findByLabelText( /Observations View/ );
|
||||
expect( observationsViewIcon ).toBeVisible( );
|
||||
const obsTaxonNameElt = await screen.findByText( mockRemoteObservation.taxon.name );
|
||||
expect( obsTaxonNameElt ).toBeTruthy( );
|
||||
expect(
|
||||
await screen.findByTestId( `ObsStatus.${mockRemoteObservation.uuid}` )
|
||||
).toBeTruthy( );
|
||||
@@ -62,9 +62,13 @@ describe( "Explore", ( ) => {
|
||||
).toBeFalsy( );
|
||||
} );
|
||||
|
||||
it( "should switch to species view list correctly", async ( ) => {
|
||||
renderAppWithComponent( <ExploreContainer /> );
|
||||
await switchToSpeciesView( );
|
||||
} );
|
||||
|
||||
it( "should display observations view grid correctly", async ( ) => {
|
||||
renderAppWithComponent( <ExploreContainer /> );
|
||||
await switchToObservationsView( );
|
||||
expect(
|
||||
await screen.findByTestId( "SegmentedButton.grid" )
|
||||
).toBeTruthy( );
|
||||
@@ -77,9 +81,12 @@ describe( "Explore", ( ) => {
|
||||
).toBeFalsy( );
|
||||
} );
|
||||
|
||||
it( "should trigger new observation fetch on pull-to-refresh", async ( ) => {
|
||||
it( "should trigger new observation fetch on pull-to-refresh in list view", async ( ) => {
|
||||
renderAppWithComponent( <ExploreContainer /> );
|
||||
await switchToObservationsView( );
|
||||
expect(
|
||||
await screen.findByTestId( "SegmentedButton.list" )
|
||||
).toBeTruthy( );
|
||||
fireEvent.press( await screen.findByTestId( "SegmentedButton.list" ) );
|
||||
|
||||
const exploreObsList = await screen.findByTestId( "ExploreObservationsAnimatedList" );
|
||||
|
||||
@@ -97,7 +104,6 @@ describe( "Explore", ( ) => {
|
||||
|
||||
it( "should trigger new observation fetch when filters change", async ( ) => {
|
||||
renderAppWithComponent( <ExploreContainer /> );
|
||||
await switchToObservationsView( );
|
||||
|
||||
// Clear the mock so we can make sure it gets called again
|
||||
inatjs.observations.search.mockClear( );
|
||||
|
||||
@@ -116,6 +116,23 @@ async function navigateToRootExplore( ) {
|
||||
await actor.press( exploreButton );
|
||||
}
|
||||
|
||||
const landOnObservationsView = async ( ) => {
|
||||
const observationsViewIcon = await screen.findByLabelText( /Observations View/ );
|
||||
expect( observationsViewIcon ).toBeVisible( );
|
||||
};
|
||||
|
||||
const switchToSpeciesView = async ( ) => {
|
||||
const observationsViewIcon = await screen.findByLabelText( /Observations View/ );
|
||||
expect( observationsViewIcon ).toBeVisible( );
|
||||
await actor.press( observationsViewIcon );
|
||||
const speciesRadioButton = await screen.findByText( "Species" );
|
||||
await actor.press( speciesRadioButton );
|
||||
const confirmButton = await screen.findByText( /EXPLORE SPECIES/ );
|
||||
await actor.press( confirmButton );
|
||||
const speciesViewIcon = await screen.findByLabelText( /Species View/ );
|
||||
expect( speciesViewIcon ).toBeVisible( );
|
||||
};
|
||||
|
||||
describe( "logged in", ( ) => {
|
||||
beforeEach( async ( ) => {
|
||||
await signIn( mockUser, { realm: global.mockRealms[__filename] } );
|
||||
@@ -240,8 +257,8 @@ describe( "logged in", ( ) => {
|
||||
it( "should navigate from TaxonDetails to Explore and back to TaxonDetails", async ( ) => {
|
||||
renderApp( );
|
||||
await navigateToRootExplore( );
|
||||
const speciesViewIcon = await screen.findByLabelText( /Species View/ );
|
||||
expect( speciesViewIcon ).toBeVisible( );
|
||||
await landOnObservationsView( );
|
||||
await switchToSpeciesView( );
|
||||
const firstTaxon = await screen.findByTestId( `TaxonGridItem.Pressable.${mockTaxon.id}` );
|
||||
await actor.press( firstTaxon );
|
||||
const taxonDetailsExploreButton = await screen.findByLabelText( /See observations of this taxon in explore/ );
|
||||
@@ -263,12 +280,7 @@ describe( "logged in", ( ) => {
|
||||
inatjs.observations.fetch.mockResolvedValue( makeResponse( mockObservations ) );
|
||||
renderApp( );
|
||||
await navigateToRootExplore( );
|
||||
const speciesViewIcon = await screen.findByLabelText( /Species View/ );
|
||||
await actor.press( speciesViewIcon );
|
||||
const observationsRadioButton = await screen.findByText( "Observations" );
|
||||
await actor.press( observationsRadioButton );
|
||||
const confirmButton = await screen.findByText( /EXPLORE OBSERVATIONS/ );
|
||||
await actor.press( confirmButton );
|
||||
await landOnObservationsView( );
|
||||
const headerCount = await screen.findByText( /1 Observation/ );
|
||||
expect( headerCount ).toBeVisible( );
|
||||
const gridView = await screen.findByTestId( "SegmentedButton.grid" );
|
||||
@@ -312,8 +324,7 @@ describe( "logged in", ( ) => {
|
||||
|
||||
renderApp( );
|
||||
await navigateToRootExplore( );
|
||||
const speciesViewIcon = await screen.findByLabelText( /Species View/ );
|
||||
expect( speciesViewIcon ).toBeVisible( );
|
||||
await landOnObservationsView( );
|
||||
const defaultNearbyLocationText = await screen.findByText( /Nearby/ );
|
||||
expect( defaultNearbyLocationText ).toBeVisible( );
|
||||
const backButton = screen.queryByTestId( "Explore.BackButton" );
|
||||
|
||||
@@ -1,87 +1,140 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { screen, userEvent } from "@testing-library/react-native";
|
||||
import * as useMapLocation from "components/Explore/hooks/useMapLocation.ts";
|
||||
import MapView from "components/Explore/MapView.tsx";
|
||||
import { ExploreProvider } from "providers/ExploreContext.tsx";
|
||||
import { EXPLORE_ACTION, ExploreProvider } from "providers/ExploreContext.tsx";
|
||||
import React from "react";
|
||||
import factory from "tests/factory";
|
||||
import { renderComponent } from "tests/helpers/render";
|
||||
|
||||
const mockData = { };
|
||||
jest.mock( "sharedHooks/useAuthenticatedQuery", () => ( {
|
||||
__esModule: true,
|
||||
default: () => ( {
|
||||
data: mockData
|
||||
} )
|
||||
} ) );
|
||||
// Mock the useExplore hook with a mock dispatch function
|
||||
const mockDispatch = jest.fn( );
|
||||
const mockDefaultExploreLocation = jest.fn( ).mockResolvedValue( {
|
||||
lat: 10,
|
||||
lng: 20
|
||||
} );
|
||||
|
||||
const mockRedoSearch = jest.fn( );
|
||||
jest.mock( "components/Explore/hooks/useMapLocation", () => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( {
|
||||
showMapBoundaryButton: false,
|
||||
redoSearchInMapArea: mockRedoSearch
|
||||
} )
|
||||
} ) );
|
||||
// Create a mock implementation of the ExploreContext
|
||||
jest.mock( "providers/ExploreContext.tsx", ( ) => {
|
||||
const originalModule = jest.requireActual( "providers/ExploreContext.tsx" );
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
useExplore: ( ) => ( {
|
||||
state: {
|
||||
lat: 10,
|
||||
lng: 20,
|
||||
placeMode: originalModule.PLACE_MODE.NEARBY,
|
||||
place: null
|
||||
},
|
||||
dispatch: mockDispatch,
|
||||
defaultExploreLocation: mockDefaultExploreLocation
|
||||
} )
|
||||
};
|
||||
} );
|
||||
|
||||
jest.mock( "sharedHooks/useLocationPermission.tsx", () => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( {
|
||||
hasPermissions: true,
|
||||
renderPermissionsGate: jest.fn(),
|
||||
requestPermissions: jest.fn()
|
||||
} )
|
||||
} ) );
|
||||
const mockObservationBounds = {
|
||||
swlat: 10,
|
||||
swlng: 20,
|
||||
nelat: 30,
|
||||
nelng: 40
|
||||
};
|
||||
|
||||
const mockRequestLocationPermissions = jest.fn( );
|
||||
|
||||
const defaultProps = {
|
||||
observationBounds: mockObservationBounds,
|
||||
queryParams: {
|
||||
taxon_id: 1,
|
||||
return_bounds: true
|
||||
},
|
||||
isLoading: false,
|
||||
hasLocationPermissions: true,
|
||||
renderLocationPermissionsGate: jest.fn( ),
|
||||
requestLocationPermissions: mockRequestLocationPermissions
|
||||
};
|
||||
|
||||
const mockObservations = [
|
||||
factory( "RemoteObservation" ),
|
||||
factory( "RemoteObservation" )
|
||||
];
|
||||
|
||||
function renderMapView() {
|
||||
renderComponent( <ExploreProvider><MapView /></ExploreProvider> );
|
||||
function renderMapView( ) {
|
||||
renderComponent( <ExploreProvider><MapView {...defaultProps} /></ExploreProvider> );
|
||||
}
|
||||
|
||||
describe( "MapView", () => {
|
||||
beforeAll( async ( ) => {
|
||||
// userEvent recommends fake timers
|
||||
jest.useFakeTimers( );
|
||||
const actor = userEvent.setup( );
|
||||
|
||||
describe( "MapView", ( ) => {
|
||||
beforeEach( () => {
|
||||
jest.useFakeTimers();
|
||||
} );
|
||||
|
||||
afterEach( () => {
|
||||
jest.clearAllMocks();
|
||||
jest.clearAllTimers();
|
||||
} );
|
||||
|
||||
it( "should be accessible", ( ) => {
|
||||
const exploreMap = (
|
||||
<ExploreProvider>
|
||||
<MapView observations={mockObservations} />
|
||||
<MapView observations={mockObservations} {...defaultProps} />
|
||||
</ExploreProvider>
|
||||
);
|
||||
expect( exploreMap ).toBeAccessible( );
|
||||
} );
|
||||
|
||||
it( "should hide redo search button by default", async ( ) => {
|
||||
renderMapView();
|
||||
it( "should hide redo search button by default", ( ) => {
|
||||
renderMapView( );
|
||||
|
||||
const redoSearchButton = screen.queryByText( /REDO SEARCH IN MAP AREA/ );
|
||||
expect( redoSearchButton ).toBeFalsy( );
|
||||
} );
|
||||
|
||||
it( "should render redo search button", async ( ) => {
|
||||
jest.spyOn( useMapLocation, "default" )
|
||||
.mockImplementation( ( ) => ( { showMapBoundaryButton: true } ) );
|
||||
renderMapView();
|
||||
const redoSearchButton = screen.queryByText( /REDO SEARCH IN MAP AREA/ );
|
||||
expect( redoSearchButton ).toBeVisible( );
|
||||
it( "should dispatch SET_EXPLORE_LOCATION when current location button is pressed", async ( ) => {
|
||||
renderMapView( );
|
||||
|
||||
const currentLocationButton = screen.getByTestId( "Map.CurrentLocationButton" );
|
||||
await actor.press( currentLocationButton );
|
||||
|
||||
await Promise.resolve( );
|
||||
jest.runAllTimers( );
|
||||
|
||||
expect( mockDefaultExploreLocation ).toHaveBeenCalled( );
|
||||
|
||||
expect( mockDispatch ).toHaveBeenCalledWith( {
|
||||
type: EXPLORE_ACTION.SET_EXPLORE_LOCATION,
|
||||
exploreLocation: { lat: 10, lng: 20 }
|
||||
} );
|
||||
} );
|
||||
|
||||
it( "should update map boundaries when redo search is pressed", async ( ) => {
|
||||
jest.spyOn( useMapLocation, "default" )
|
||||
.mockImplementation( ( ) => ( {
|
||||
showMapBoundaryButton: true,
|
||||
redoSearchInMapArea: mockRedoSearch
|
||||
} ) );
|
||||
renderMapView();
|
||||
it( "should dispatch requestLocationPermissions when current location button "
|
||||
+ " is pressed and user has not given permissions", async ( ) => {
|
||||
renderComponent(
|
||||
<ExploreProvider>
|
||||
<MapView
|
||||
{...defaultProps}
|
||||
hasLocationPermissions={false}
|
||||
/>
|
||||
</ExploreProvider>
|
||||
);
|
||||
|
||||
const actor = await userEvent.setup( );
|
||||
const redoSearchButton = screen.queryByText( /REDO SEARCH IN MAP AREA/ );
|
||||
await actor.press( redoSearchButton );
|
||||
const currentLocationButton = screen.getByTestId( "Map.CurrentLocationButton" );
|
||||
await actor.press( currentLocationButton );
|
||||
|
||||
expect( mockRedoSearch ).toHaveBeenCalled( );
|
||||
await Promise.resolve( );
|
||||
jest.runAllTimers( );
|
||||
|
||||
expect( mockRequestLocationPermissions ).toHaveBeenCalled( );
|
||||
} );
|
||||
|
||||
it( "should show loading indicator when isLoading is true", ( ) => {
|
||||
renderComponent(
|
||||
<ExploreProvider>
|
||||
<MapView {...defaultProps} isLoading />
|
||||
</ExploreProvider>
|
||||
);
|
||||
|
||||
const loadingIndicator = screen.getByTestId( "activity-indicator" );
|
||||
expect( loadingIndicator ).toBeTruthy( );
|
||||
} );
|
||||
} );
|
||||
|
||||
Reference in New Issue
Block a user