mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-06-19 13:11:23 -04:00
Merge branch 'main' into obs-detail
This commit is contained in:
@@ -7,18 +7,19 @@ import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { useTheme } from "react-native-paper";
|
||||
|
||||
import WarningText from "./WarningText";
|
||||
|
||||
type Props = {
|
||||
accuracyTest: string,
|
||||
containerStyle: Object
|
||||
getShadow: Function
|
||||
};
|
||||
|
||||
const CrosshairCircle = ( { accuracyTest, containerStyle }: Props ): Node => {
|
||||
const CrosshairCircle = ( { accuracyTest, getShadow }: Props ): Node => {
|
||||
const theme = useTheme( );
|
||||
|
||||
return (
|
||||
<View
|
||||
className="absolute"
|
||||
style={containerStyle}
|
||||
className="right-[127px] bottom-[127px]"
|
||||
pointerEvents="none"
|
||||
>
|
||||
<View
|
||||
@@ -54,6 +55,9 @@ const CrosshairCircle = ( { accuracyTest, containerStyle }: Props ): Node => {
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View className="absolute m-auto left-0 right-0 top-[300px]">
|
||||
<WarningText accuracyTest={accuracyTest} getShadow={getShadow} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import {
|
||||
Button, StickyToolbar
|
||||
} from "components/SharedComponents";
|
||||
import { Button } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, {
|
||||
useContext
|
||||
} from "react";
|
||||
import React, { useContext } from "react";
|
||||
import useTranslation from "sharedHooks/useTranslation";
|
||||
|
||||
type Props = {
|
||||
@@ -24,9 +21,9 @@ const Footer = ( { keysToUpdate, goBackOnSave }: Props ): Node => {
|
||||
} = useContext( ObsEditContext );
|
||||
|
||||
return (
|
||||
<StickyToolbar>
|
||||
<View className="h-[73px] justify-center">
|
||||
<Button
|
||||
className="px-[25px]"
|
||||
className="mx-[25px]"
|
||||
onPress={( ) => {
|
||||
updateObservationKeys( keysToUpdate );
|
||||
if ( goBackOnSave ) {
|
||||
@@ -37,7 +34,7 @@ const Footer = ( { keysToUpdate, goBackOnSave }: Props ): Node => {
|
||||
text={t( "SAVE-LOCATION" )}
|
||||
level="neutral"
|
||||
/>
|
||||
</StickyToolbar>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
14
src/components/LocationPicker/LoadingIndicator.js
Normal file
14
src/components/LocationPicker/LoadingIndicator.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import { View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { ActivityIndicator } from "react-native-paper";
|
||||
|
||||
const LoadingIndicator = ( ): Node => (
|
||||
<View className="h-[80px] w-[80px] bg-white right-[40px] bottom-[40px] justify-center">
|
||||
<ActivityIndicator large />
|
||||
</View>
|
||||
);
|
||||
|
||||
export default LoadingIndicator;
|
||||
@@ -1,30 +1,27 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import {
|
||||
Body3,
|
||||
CloseButton, Heading4,
|
||||
INatIconButton,
|
||||
ViewWrapper
|
||||
} from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext, useEffect, useRef, useState
|
||||
} from "react";
|
||||
import { Dimensions } from "react-native";
|
||||
import React from "react";
|
||||
import MapView from "react-native-maps";
|
||||
import { IconButton, useTheme } from "react-native-paper";
|
||||
import fetchPlaceName from "sharedHelpers/fetchPlaceName";
|
||||
import fetchUserLocation from "sharedHelpers/fetchUserLocation";
|
||||
import { useTheme } from "react-native-paper";
|
||||
import useTranslation from "sharedHooks/useTranslation";
|
||||
import { getShadowStyle } from "styles/global";
|
||||
|
||||
import CrosshairCircle from "./CrosshairCircle";
|
||||
import DisplayLatLng from "./DisplayLatLng";
|
||||
import Footer from "./Footer";
|
||||
import LoadingIndicator from "./LoadingIndicator";
|
||||
import LocationSearch from "./LocationSearch";
|
||||
import WarningText from "./WarningText";
|
||||
|
||||
export const DESIRED_LOCATION_ACCURACY = 100;
|
||||
export const REQUIRED_LOCATION_ACCURACY = 500000;
|
||||
|
||||
const getShadow = shadowColor => getShadowStyle( {
|
||||
shadowColor,
|
||||
@@ -35,152 +32,131 @@ const getShadow = shadowColor => getShadowStyle( {
|
||||
elevation: 5
|
||||
} );
|
||||
|
||||
const { width, height } = Dimensions.get( "screen" );
|
||||
|
||||
const DELTA = 0.2;
|
||||
const CROSSHAIRLENGTH = 254;
|
||||
export const DESIRED_LOCATION_ACCURACY = 100;
|
||||
export const REQUIRED_LOCATION_ACCURACY = 500000;
|
||||
|
||||
type Props = {
|
||||
route: {
|
||||
params: {
|
||||
goBackOnSave: boolean
|
||||
},
|
||||
},
|
||||
showMap: boolean,
|
||||
loading: boolean,
|
||||
accuracyTest: string,
|
||||
region: Object,
|
||||
mapView: any,
|
||||
updateRegion: Function,
|
||||
locationName: ?string,
|
||||
updateLocationName: Function,
|
||||
accuracy: number,
|
||||
returnToUserLocation: Function,
|
||||
keysToUpdate: Object,
|
||||
goBackOnSave: Function,
|
||||
toggleMapLayer: Function,
|
||||
mapType: string,
|
||||
setMapReady: Function,
|
||||
selectPlaceResult: Function,
|
||||
hidePlaceResults: boolean
|
||||
};
|
||||
|
||||
const centerCrosshair = ( height / 2 ) - CROSSHAIRLENGTH + 30;
|
||||
|
||||
const LocationPicker = ( { route }: Props ): Node => {
|
||||
const LocationPicker = ( {
|
||||
showMap,
|
||||
loading,
|
||||
accuracyTest,
|
||||
region,
|
||||
mapView,
|
||||
updateRegion,
|
||||
locationName,
|
||||
updateLocationName,
|
||||
accuracy,
|
||||
returnToUserLocation,
|
||||
keysToUpdate,
|
||||
goBackOnSave,
|
||||
toggleMapLayer,
|
||||
mapType,
|
||||
setMapReady,
|
||||
selectPlaceResult,
|
||||
hidePlaceResults
|
||||
}: Props ): Node => {
|
||||
const theme = useTheme( );
|
||||
const { t } = useTranslation( );
|
||||
const mapView = useRef( );
|
||||
const { currentObservation } = useContext( ObsEditContext );
|
||||
const navigation = useNavigation( );
|
||||
const { goBackOnSave } = route.params;
|
||||
const [mapType, setMapType] = useState( "standard" );
|
||||
const [locationName, setLocationName] = useState( currentObservation?.place_guess );
|
||||
const [accuracy, setAccuracy] = useState( currentObservation?.positional_accuracy );
|
||||
const [accuracyTest, setAccuracyTest] = useState( "pass" );
|
||||
const [region, setRegion] = useState( {
|
||||
latitude: currentObservation?.latitude || 0.0,
|
||||
longitude: currentObservation?.longitude || 0.0,
|
||||
latitudeDelta: DELTA,
|
||||
longitudeDelta: DELTA
|
||||
} );
|
||||
|
||||
const keysToUpdate = {
|
||||
latitude: region.latitude,
|
||||
longitude: region.longitude,
|
||||
positional_accuracy: accuracy,
|
||||
place_guess: locationName
|
||||
};
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( accuracy < DESIRED_LOCATION_ACCURACY ) {
|
||||
setAccuracyTest( "pass" );
|
||||
} else if ( accuracy < REQUIRED_LOCATION_ACCURACY ) {
|
||||
setAccuracyTest( "acceptable" );
|
||||
} else {
|
||||
setAccuracyTest( "fail" );
|
||||
}
|
||||
}, [accuracy] );
|
||||
|
||||
const updateRegion = async newRegion => {
|
||||
const estimatedAccuracy = newRegion.longitudeDelta * 1000 * (
|
||||
( CROSSHAIRLENGTH / width ) * 100
|
||||
);
|
||||
|
||||
const placeName = await fetchPlaceName( newRegion.latitude, newRegion.longitude );
|
||||
if ( placeName ) {
|
||||
setLocationName( placeName );
|
||||
}
|
||||
setRegion( newRegion );
|
||||
setAccuracy( estimatedAccuracy );
|
||||
};
|
||||
|
||||
const renderBackButton = useCallback(
|
||||
( ) => <CloseButton black className="absolute" size={19} />,
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect( ( ) => {
|
||||
const renderHeaderTitle = ( ) => <Heading4>{t( "EDIT-LOCATION" )}</Heading4>;
|
||||
|
||||
const headerOptions = {
|
||||
headerRight: renderBackButton,
|
||||
headerTitle: renderHeaderTitle
|
||||
};
|
||||
|
||||
navigation.setOptions( headerOptions );
|
||||
}, [renderBackButton, navigation, t] );
|
||||
|
||||
const toggleMapLayer = ( ) => {
|
||||
if ( mapType === "standard" ) {
|
||||
setMapType( "satellite" );
|
||||
} else {
|
||||
setMapType( "standard" );
|
||||
}
|
||||
};
|
||||
|
||||
const returnToUserLocation = async ( ) => {
|
||||
const userLocation = await fetchUserLocation( );
|
||||
setRegion( {
|
||||
...region,
|
||||
latitude: userLocation?.latitude,
|
||||
longitude: userLocation?.longitude
|
||||
} );
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper testID="location-picker">
|
||||
<MapView
|
||||
className="flex-1"
|
||||
showsCompass={false}
|
||||
region={region}
|
||||
ref={mapView}
|
||||
mapType={mapType}
|
||||
onRegionChangeComplete={async newRegion => {
|
||||
updateRegion( newRegion );
|
||||
// console.log( await mapView?.current?.getMapBoundaries( ) );
|
||||
}}
|
||||
/>
|
||||
<CrosshairCircle
|
||||
accuracyTest={accuracyTest}
|
||||
// eslint-disable-next-line react-native/no-inline-styles
|
||||
containerStyle={{
|
||||
alignSelf: "center",
|
||||
top: centerCrosshair
|
||||
}}
|
||||
/>
|
||||
<LocationSearch
|
||||
region={region}
|
||||
setRegion={setRegion}
|
||||
locationName={locationName}
|
||||
setLocationName={setLocationName}
|
||||
getShadow={getShadow}
|
||||
/>
|
||||
<DisplayLatLng
|
||||
region={region}
|
||||
accuracy={accuracy}
|
||||
getShadow={getShadow}
|
||||
/>
|
||||
<WarningText accuracyTest={accuracyTest} getShadow={getShadow} />
|
||||
<View style={getShadow( theme.colors.primary )}>
|
||||
<IconButton
|
||||
className="absolute bottom-20 bg-white left-2"
|
||||
icon="layers"
|
||||
onPress={toggleMapLayer}
|
||||
<ViewWrapper testID="location-picker" className="flex-1">
|
||||
<View className="justify-center">
|
||||
<Heading4 className="self-center my-4">{t( "EDIT-LOCATION" )}</Heading4>
|
||||
<View className="absolute right-2">
|
||||
<CloseButton black size={19} />
|
||||
</View>
|
||||
</View>
|
||||
<View className="z-20">
|
||||
<LocationSearch
|
||||
locationName={locationName}
|
||||
updateLocationName={updateLocationName}
|
||||
getShadow={getShadow}
|
||||
selectPlaceResult={selectPlaceResult}
|
||||
hidePlaceResults={hidePlaceResults}
|
||||
/>
|
||||
</View>
|
||||
<View style={getShadow( theme.colors.primary )}>
|
||||
<IconButton
|
||||
className="absolute bottom-20 bg-white right-2"
|
||||
icon="location-crosshairs"
|
||||
onPress={returnToUserLocation}
|
||||
<View className="z-10">
|
||||
<DisplayLatLng
|
||||
region={region}
|
||||
accuracy={accuracy}
|
||||
getShadow={getShadow}
|
||||
/>
|
||||
</View>
|
||||
<View className="top-1/2 left-1/2 absolute z-10">
|
||||
{showMap && (
|
||||
<CrosshairCircle
|
||||
accuracyTest={accuracyTest}
|
||||
getShadow={getShadow}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View className="top-1/2 left-1/2 absolute z-10">
|
||||
{loading && <LoadingIndicator />}
|
||||
</View>
|
||||
<View className="flex-shrink">
|
||||
{showMap
|
||||
? (
|
||||
<MapView
|
||||
className="h-full"
|
||||
showsCompass={false}
|
||||
region={region}
|
||||
ref={mapView}
|
||||
mapType={mapType}
|
||||
// TODO: figure out the right zoom level here
|
||||
// don't think it's necessary to let a user zoom out far beyond cities
|
||||
minZoomLevel={5}
|
||||
onRegionChangeComplete={async newRegion => {
|
||||
updateRegion( newRegion );
|
||||
}}
|
||||
onMapReady={setMapReady}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<View className="h-full bg-lightGray items-center justify-center">
|
||||
<Body3>{t( "Try-searching-for-a-location-name" )}</Body3>
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={getShadow( theme.colors.primary )}
|
||||
className="absolute bottom-3 bg-white left-3 rounded-full"
|
||||
>
|
||||
<INatIconButton
|
||||
icon="layers"
|
||||
onPress={toggleMapLayer}
|
||||
height={46}
|
||||
width={46}
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={getShadow( theme.colors.primary )}
|
||||
className="absolute bottom-3 bg-white right-3 rounded-full"
|
||||
>
|
||||
<INatIconButton
|
||||
icon="location-crosshairs"
|
||||
onPress={returnToUserLocation}
|
||||
height={46}
|
||||
width={46}
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Footer
|
||||
keysToUpdate={keysToUpdate}
|
||||
goBackOnSave={goBackOnSave}
|
||||
|
||||
261
src/components/LocationPicker/LocationPickerContainer.js
Normal file
261
src/components/LocationPicker/LocationPickerContainer.js
Normal file
@@ -0,0 +1,261 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext, useEffect,
|
||||
useReducer, useRef
|
||||
} from "react";
|
||||
import { Dimensions, Platform } from "react-native";
|
||||
import fetchPlaceName from "sharedHelpers/fetchPlaceName";
|
||||
import fetchUserLocation from "sharedHelpers/fetchUserLocation";
|
||||
|
||||
import LocationPicker from "./LocationPicker";
|
||||
|
||||
const { width } = Dimensions.get( "screen" );
|
||||
|
||||
const DELTA = 0.02;
|
||||
const CROSSHAIRLENGTH = 254;
|
||||
export const DESIRED_LOCATION_ACCURACY = 100;
|
||||
export const REQUIRED_LOCATION_ACCURACY = 500000;
|
||||
|
||||
const estimatedAccuracy = longitudeDelta => longitudeDelta * 1000 * (
|
||||
( CROSSHAIRLENGTH / width ) * 100 );
|
||||
|
||||
const initialState = {
|
||||
accuracy: 0,
|
||||
accuracyTest: "pass",
|
||||
locationName: null,
|
||||
mapType: "standard",
|
||||
region: {
|
||||
latitude: 0.0,
|
||||
longitude: 0.0,
|
||||
latitudeDelta: DELTA,
|
||||
longitudeDelta: DELTA
|
||||
},
|
||||
loading: true,
|
||||
hidePlaceResults: true
|
||||
};
|
||||
|
||||
const reducer = ( state, action ) => {
|
||||
console.log( action.type, "action type" );
|
||||
switch ( action.type ) {
|
||||
case "ESTIMATE_ACCURACY":
|
||||
return {
|
||||
...state,
|
||||
positional_accuracy: estimatedAccuracy( state.region.longitudeDelta )
|
||||
};
|
||||
case "FETCH_CURRENT_LOCATION":
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
region: {
|
||||
...state.region,
|
||||
latitude: action.userLocation?.latitude,
|
||||
longitude: action.userLocation?.longitude
|
||||
}
|
||||
};
|
||||
case "INITIALIZE_MAP":
|
||||
return {
|
||||
...state,
|
||||
accuracy: action.currentObservation?.positional_accuracy,
|
||||
locationName: action.currentObservation?.place_guess,
|
||||
region: {
|
||||
...state.region,
|
||||
latitude: action.currentObservation?.latitude,
|
||||
longitude: action.currentObservation?.longitude,
|
||||
latitudeDelta: DELTA,
|
||||
longitudeDelta: DELTA
|
||||
}
|
||||
};
|
||||
case "RESET_LOCATION_PICKER":
|
||||
return {
|
||||
...action.initialState
|
||||
};
|
||||
case "SELECT_PLACE_RESULT":
|
||||
return {
|
||||
...state,
|
||||
locationName: action.locationName,
|
||||
region: action.region,
|
||||
hidePlaceResults: true
|
||||
};
|
||||
case "SET_ACCURACY_TEST":
|
||||
return {
|
||||
...state,
|
||||
accuracyTest: action.accuracyTest
|
||||
};
|
||||
case "SET_LOADING":
|
||||
return {
|
||||
...state,
|
||||
loading: action.loading
|
||||
};
|
||||
case "SET_MAP_TYPE":
|
||||
return {
|
||||
...state,
|
||||
mapType: action.mapType
|
||||
};
|
||||
case "UPDATE_LOCATION_NAME":
|
||||
return {
|
||||
...state,
|
||||
locationName: action.locationName,
|
||||
hidePlaceResults: false
|
||||
};
|
||||
case "UPDATE_REGION":
|
||||
return {
|
||||
...state,
|
||||
locationName: action.locationName,
|
||||
region: action.region,
|
||||
accuracy: action.accuracy
|
||||
};
|
||||
default:
|
||||
throw new Error( );
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
route: {
|
||||
params: {
|
||||
goBackOnSave: boolean
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const LocationPickerContainer = ( { route }: Props ): Node => {
|
||||
const mapView = useRef( );
|
||||
const { currentObservation } = useContext( ObsEditContext );
|
||||
const navigation = useNavigation( );
|
||||
const { goBackOnSave } = route.params;
|
||||
|
||||
const [state, dispatch] = useReducer( reducer, initialState );
|
||||
|
||||
const {
|
||||
accuracy,
|
||||
accuracyTest,
|
||||
loading,
|
||||
locationName,
|
||||
mapType,
|
||||
region,
|
||||
hidePlaceResults
|
||||
} = state;
|
||||
|
||||
const showMap = region.latitude !== 0.0;
|
||||
|
||||
const keysToUpdate = {
|
||||
latitude: region.latitude,
|
||||
longitude: region.longitude,
|
||||
positional_accuracy: accuracy,
|
||||
place_guess: locationName
|
||||
};
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( accuracy < DESIRED_LOCATION_ACCURACY ) {
|
||||
dispatch( { type: "SET_ACCURACY_TEST", accuracyTest: "pass" } );
|
||||
} else if ( accuracy < REQUIRED_LOCATION_ACCURACY ) {
|
||||
dispatch( { type: "SET_ACCURACY_TEST", accuracyTest: "acceptable" } );
|
||||
} else {
|
||||
dispatch( { type: "SET_ACCURACY_TEST", accuracyTest: "fail" } );
|
||||
}
|
||||
}, [accuracy] );
|
||||
|
||||
const updateRegion = async newRegion => {
|
||||
const newAccuracy = estimatedAccuracy( newRegion.longitudeDelta );
|
||||
|
||||
// don't update region if map hasn't actually moved
|
||||
// otherwise, it's jittery on Android
|
||||
if ( newRegion.latitude.toFixed( 6 ) === region.latitude?.toFixed( 6 )
|
||||
&& newRegion.longitude.toFixed( 6 ) === region.longitude?.toFixed( 6 )
|
||||
&& newRegion.latitudeDelta.toFixed( 6 ) === region.latitudeDelta?.toFixed( 6 ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const placeName = await fetchPlaceName( newRegion.latitude, newRegion.longitude );
|
||||
dispatch( {
|
||||
type: "UPDATE_REGION",
|
||||
locationName: placeName || null,
|
||||
region: newRegion,
|
||||
accuracy: newAccuracy
|
||||
} );
|
||||
};
|
||||
|
||||
const toggleMapLayer = ( ) => {
|
||||
if ( mapType === "standard" ) {
|
||||
dispatch( { type: "SET_MAP_TYPE", mapType: "satellite" } );
|
||||
} else {
|
||||
dispatch( { type: "SET_MAP_TYPE", mapType: "standard" } );
|
||||
}
|
||||
};
|
||||
|
||||
const returnToUserLocation = async ( ) => {
|
||||
dispatch( { type: "SET_LOADING", loading: false } );
|
||||
const userLocation = await fetchUserLocation( );
|
||||
dispatch( { type: "FETCH_CURRENT_LOCATION", userLocation } );
|
||||
mapView.current?.getCamera().then( cam => {
|
||||
if ( Platform.OS === "android" ) {
|
||||
cam.zoom = 20;
|
||||
} else {
|
||||
cam.altitude = 100;
|
||||
}
|
||||
mapView.current?.animateCamera( cam );
|
||||
} );
|
||||
dispatch( { type: "ESTIMATE_ACCURACY" } );
|
||||
};
|
||||
|
||||
const updateLocationName = useCallback( name => {
|
||||
dispatch( { type: "UPDATE_LOCATION_NAME", locationName: name } );
|
||||
}, [] );
|
||||
|
||||
// reset to initialState when exiting screen without saving
|
||||
useEffect(
|
||||
( ) => {
|
||||
navigation.addListener( "focus", ( ) => {
|
||||
if ( !currentObservation ) { return; }
|
||||
dispatch( { type: "INITIALIZE_MAP", currentObservation } );
|
||||
} );
|
||||
navigation.addListener( "blur", ( ) => {
|
||||
dispatch( { type: "RESET_LOCATION_PICKER", initialState } );
|
||||
} );
|
||||
},
|
||||
[navigation, currentObservation]
|
||||
);
|
||||
|
||||
const setMapReady = ( ) => dispatch( { type: "SET_LOADING", loading: false } );
|
||||
|
||||
const selectPlaceResult = place => {
|
||||
const { coordinates } = place.point_geojson;
|
||||
dispatch( {
|
||||
type: "SELECT_PLACE_RESULT",
|
||||
locationName: place.display_name,
|
||||
region: {
|
||||
...region,
|
||||
latitude: coordinates[1],
|
||||
longitude: coordinates[0]
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
return (
|
||||
<LocationPicker
|
||||
showMap={showMap}
|
||||
loading={loading}
|
||||
accuracyTest={accuracyTest}
|
||||
region={region}
|
||||
mapView={mapView}
|
||||
updateRegion={updateRegion}
|
||||
locationName={locationName}
|
||||
updateLocationName={updateLocationName}
|
||||
accuracy={accuracy}
|
||||
returnToUserLocation={returnToUserLocation}
|
||||
keysToUpdate={keysToUpdate}
|
||||
goBackOnSave={goBackOnSave}
|
||||
toggleMapLayer={toggleMapLayer}
|
||||
mapType={mapType}
|
||||
setMapReady={setMapReady}
|
||||
selectPlaceResult={selectPlaceResult}
|
||||
hidePlaceResults={hidePlaceResults}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocationPickerContainer;
|
||||
@@ -8,24 +8,25 @@ import {
|
||||
} from "components/SharedComponents";
|
||||
import { Pressable, View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React, { useState } from "react";
|
||||
import React, { useRef } from "react";
|
||||
import { Keyboard } from "react-native";
|
||||
import { useTheme } from "react-native-paper";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
|
||||
type Props = {
|
||||
region: Object,
|
||||
setRegion: Function,
|
||||
locationName: string,
|
||||
setLocationName: Function,
|
||||
getShadow: Function
|
||||
locationName: ?string,
|
||||
updateLocationName: Function,
|
||||
getShadow: Function,
|
||||
selectPlaceResult: Function,
|
||||
hidePlaceResults: boolean
|
||||
};
|
||||
|
||||
const LocationSearch = ( {
|
||||
region, setRegion, locationName, setLocationName, getShadow
|
||||
locationName, updateLocationName, getShadow, selectPlaceResult, hidePlaceResults
|
||||
}: Props ): Node => {
|
||||
const [hideResults, setHideResults] = useState( false );
|
||||
const theme = useTheme( );
|
||||
const queryClient = useQueryClient( );
|
||||
const locationInput = useRef( );
|
||||
|
||||
// this seems necessary for clearing the cache between searches
|
||||
queryClient.invalidateQueries( ["fetchSearchResults"] );
|
||||
@@ -45,32 +46,30 @@ const LocationSearch = ( {
|
||||
<>
|
||||
<SearchBar
|
||||
handleTextChange={locationText => {
|
||||
setLocationName( locationText );
|
||||
setHideResults( false );
|
||||
// only update location name when a user is typing,
|
||||
// not when a user selects a location from the dropdown
|
||||
if ( locationInput?.current?.isFocused( ) ) {
|
||||
updateLocationName( locationText );
|
||||
}
|
||||
}}
|
||||
value={locationName}
|
||||
testID="LocationPicker.locationSearch"
|
||||
containerClass="absolute top-[20px] right-[26px] left-[26px]"
|
||||
hasShadow
|
||||
input={locationInput}
|
||||
/>
|
||||
<View
|
||||
className="absolute top-[65px] right-[26px] left-[26px] bg-white rounded-lg z-50"
|
||||
className="absolute top-[65px] right-[26px] left-[26px] bg-white rounded-lg z-100"
|
||||
style={getShadow( theme.colors.primary )}
|
||||
>
|
||||
{!hideResults && placeResults?.map( place => (
|
||||
{!hidePlaceResults && placeResults?.map( place => (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
key={place.id}
|
||||
className="p-2 border-[0.5px] border-lightGray"
|
||||
onPress={( ) => {
|
||||
setHideResults( true );
|
||||
setLocationName( place.display_name );
|
||||
const { coordinates } = place.point_geojson;
|
||||
setRegion( {
|
||||
...region,
|
||||
latitude: coordinates[1],
|
||||
longitude: coordinates[0]
|
||||
} );
|
||||
selectPlaceResult( place );
|
||||
Keyboard.dismiss( );
|
||||
}}
|
||||
>
|
||||
<Body3>{place.display_name}</Body3>
|
||||
|
||||
@@ -30,26 +30,26 @@ const WarningText = ( { accuracyTest, getShadow }: Props ): Node => {
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="justify-center items-center" style={getShadow( theme.colors.primary )}>
|
||||
<View
|
||||
className={classnames( "p-4 rounded-xl bottom-[180px] max-w-[316px]", {
|
||||
"bg-transparent": accuracyTest === "pass",
|
||||
"bg-white": accuracyTest === "acceptable",
|
||||
"bg-warningRed": accuracyTest === "fail"
|
||||
} )}
|
||||
<View
|
||||
pointerEvents="none"
|
||||
className={classnames( "p-4 rounded-xl", {
|
||||
"bg-transparent": accuracyTest === "pass",
|
||||
"bg-white": accuracyTest === "acceptable",
|
||||
"bg-warningRed": accuracyTest === "fail"
|
||||
} )}
|
||||
style={getShadow( theme.colors.primary )}
|
||||
>
|
||||
<Body3
|
||||
className={classnames(
|
||||
"text-black",
|
||||
"text-center",
|
||||
{
|
||||
"text-white": accuracyTest === "fail"
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Body3
|
||||
className={classnames(
|
||||
"text-black",
|
||||
"text-center",
|
||||
{
|
||||
"text-white": accuracyTest === "fail"
|
||||
}
|
||||
)}
|
||||
>
|
||||
{displayWarningText( )}
|
||||
</Body3>
|
||||
</View>
|
||||
{displayWarningText( )}
|
||||
</Body3>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -91,14 +91,19 @@ const MyObservations = ( {
|
||||
// basing collapsible sticky header code off the example in this article
|
||||
// https://medium.com/swlh/making-a-collapsible-sticky-header-animations-with-react-native-6ad7763875c3
|
||||
const scrollY = useRef( new Animated.Value( 0 ) );
|
||||
|
||||
// On Android, the scroll view offset is a double (not an integer), and interpolation shouldn't be
|
||||
// one-to-one, which causes a jittery header while slow scrolling (see issue #634).
|
||||
// See here as well: https://stackoverflow.com/a/60898411/1233767
|
||||
const scrollYClamped = diffClamp(
|
||||
scrollY.current,
|
||||
0,
|
||||
heightAboveToolbar
|
||||
heightAboveToolbar * 2
|
||||
);
|
||||
|
||||
// Same as comment above (see here: https://stackoverflow.com/a/60898411/1233767)
|
||||
const offsetForHeader = scrollYClamped.interpolate( {
|
||||
inputRange: [0, heightAboveToolbar],
|
||||
inputRange: [0, heightAboveToolbar * 2],
|
||||
// $FlowIgnore
|
||||
outputRange: [0, -heightAboveToolbar]
|
||||
} );
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import ActivityHeader from "components/ObsDetails/ActivityHeader";
|
||||
import AgreeWithIDSheet from "components/ObsDetails/Sheets/AgreeWithIDSheet";
|
||||
import { DisplayTaxonName, Divider } from "components/SharedComponents";
|
||||
import INatIcon from "components/SharedComponents/INatIcon";
|
||||
import UserText from "components/SharedComponents/UserText";
|
||||
import {
|
||||
DisplayTaxonName, Divider, INatIcon, UserText
|
||||
} from "components/SharedComponents";
|
||||
import {
|
||||
Pressable, View
|
||||
} from "components/styledComponents";
|
||||
@@ -28,21 +28,22 @@ type Props = {
|
||||
onAgree: Function,
|
||||
currentUserId?: Number,
|
||||
observationUUID: string,
|
||||
userAgreedId?: string
|
||||
// userAgreedId?: string
|
||||
}
|
||||
|
||||
const ActivityItem = ( {
|
||||
item, navToTaxonDetails, toggleRefetch, refetchRemoteObservation, onAgree, currentUserId,
|
||||
observationUUID, userAgreedId
|
||||
observationUUID
|
||||
}: Props ): Node => {
|
||||
const { taxon, user } = item;
|
||||
const isOnline = useIsConnected( );
|
||||
const userId = currentUserId;
|
||||
const [hideAgreeWithIdSheet, setHideAgreeWithIdSheet] = useState( true );
|
||||
const showAgreeButton = taxon && user && user.id !== userId && taxon.rank_level <= 10;
|
||||
const [showAgreeWithIdSheet, setShowAgreeWithIdSheet] = useState( false );
|
||||
const [showCommentBox, setShowCommentBox] = useState( false );
|
||||
const [comment, setComment] = useState( "" );
|
||||
const showAgree = taxon && user && user.id !== userId && taxon.rank_level <= 10
|
||||
&& userAgreedId !== taxon?.id;
|
||||
// const showAgree = taxon && user && user.id !== userId && taxon.rank_level <= 10
|
||||
// && userAgreedId !== taxon?.id;
|
||||
|
||||
const isCurrent = item.current !== undefined
|
||||
? item.current
|
||||
@@ -69,7 +70,7 @@ const ActivityItem = ( {
|
||||
};
|
||||
|
||||
onAgree( agreeParams );
|
||||
setHideAgreeWithIdSheet( true );
|
||||
setShowAgreeWithIdSheet( false );
|
||||
};
|
||||
|
||||
const openCommentBox = () => setShowCommentBox( true );
|
||||
@@ -80,11 +81,11 @@ const ActivityItem = ( {
|
||||
|
||||
const agreeIdSheetDiscardChanges = () => {
|
||||
setComment( "" );
|
||||
setHideAgreeWithIdSheet( true );
|
||||
setShowAgreeWithIdSheet( false );
|
||||
};
|
||||
|
||||
const onIDAgreePressed = () => {
|
||||
setHideAgreeWithIdSheet( false );
|
||||
setShowAgreeWithIdSheet( true );
|
||||
};
|
||||
|
||||
// const renderTaxonImage = () => {
|
||||
@@ -122,11 +123,11 @@ const ActivityItem = ( {
|
||||
layout="horizontal"
|
||||
/>
|
||||
</Pressable>
|
||||
{ showAgree && (
|
||||
{ showAgreeButton && (
|
||||
<Pressable
|
||||
testID="ActivityItem.AgreeIdButton"
|
||||
accessibilityRole="button"
|
||||
onPress={() => onIDAgreePressed( )}
|
||||
onPress={onIDAgreePressed}
|
||||
>
|
||||
<INatIcon name="id-agree" size={33} />
|
||||
</Pressable>
|
||||
@@ -140,12 +141,12 @@ const ActivityItem = ( {
|
||||
)}
|
||||
<Divider />
|
||||
<AgreeWithIDSheet
|
||||
hide={hideAgreeWithIdSheet}
|
||||
showAgreeWithIdSheet={showAgreeWithIdSheet}
|
||||
comment={comment}
|
||||
openCommentBox={openCommentBox}
|
||||
taxon={taxon}
|
||||
discardChanges={() => agreeIdSheetDiscardChanges( )}
|
||||
handleClose={() => agreeIdSheetDiscardChanges( )}
|
||||
discardChanges={agreeIdSheetDiscardChanges}
|
||||
handleClose={agreeIdSheetDiscardChanges}
|
||||
onAgree={onAgreePressed}
|
||||
/>
|
||||
<AddCommentModal
|
||||
|
||||
@@ -77,15 +77,15 @@ const ActivityTab = ( {
|
||||
createIdentificationMutation.mutate( { identification: agreeParams } );
|
||||
};
|
||||
|
||||
const findRecentUserAgreedToID = () => {
|
||||
const currentIds = observation?.identifications;
|
||||
const userAgree = currentIds.filter( id => id.user?.id === userId );
|
||||
return userAgree.length > 0
|
||||
? userAgree[userAgree.length - 1].taxon.id
|
||||
: undefined;
|
||||
};
|
||||
// const findRecentUserAgreedToID = () => {
|
||||
// const currentIds = observation?.identifications;
|
||||
// const userAgree = currentIds.filter( id => id.user?.id === userId );
|
||||
// return userAgree.length > 0
|
||||
// ? userAgree[userAgree.length - 1].taxon.id
|
||||
// : undefined;
|
||||
// };
|
||||
|
||||
const userAgreedToId = findRecentUserAgreedToID();
|
||||
// const userAgreedToId = findRecentUserAgreedToID();
|
||||
|
||||
useEffect( ( ) => {
|
||||
// set initial ids for activity tab
|
||||
@@ -108,7 +108,7 @@ const ActivityTab = ( {
|
||||
// https://github.com/inaturalist/inaturalist/blob/df6572008f60845b8ef5972a92a9afbde6f67829/app/webpack/observations/show/components/activity_item.jsx
|
||||
const activitytemsList = activityItems.map( item => (
|
||||
<ActivityItem
|
||||
userAgreedId={userAgreedToId}
|
||||
// userAgreedId={userAgreedToId}
|
||||
key={item.uuid}
|
||||
observationUUID={uuid}
|
||||
item={item}
|
||||
|
||||
@@ -19,7 +19,7 @@ import Taxon from "realmModels/Taxon";
|
||||
handleClose: Function,
|
||||
taxon: Object,
|
||||
discardChanges: Function,
|
||||
hide: boolean,
|
||||
showAgreeWithIdSheet: boolean,
|
||||
comment:string,
|
||||
openCommentBox: Function
|
||||
}
|
||||
@@ -54,7 +54,7 @@ const AgreeWithIDSheet = ( {
|
||||
handleClose,
|
||||
discardChanges,
|
||||
taxon,
|
||||
hide,
|
||||
showAgreeWithIdSheet,
|
||||
comment,
|
||||
openCommentBox
|
||||
}: Props ): Node => {
|
||||
@@ -70,7 +70,7 @@ const AgreeWithIDSheet = ( {
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
hide={hide}
|
||||
hidden={!showAgreeWithIdSheet}
|
||||
handleClose={handleClose}
|
||||
confirm={discardChanges}
|
||||
headerText={t( "AGREE-WITH-ID" )}
|
||||
@@ -81,7 +81,7 @@ const AgreeWithIDSheet = ( {
|
||||
<View
|
||||
className="mx-[26px] space-y-[11px] my-[15px]"
|
||||
>
|
||||
<List2>
|
||||
<List2 className="text-black">
|
||||
{t( "Agree-with-ID-description" )}
|
||||
</List2>
|
||||
{ comment && (
|
||||
@@ -89,20 +89,20 @@ const AgreeWithIDSheet = ( {
|
||||
className=" flex-row items-center bg-lightGray p-[15px] rounded"
|
||||
>
|
||||
<INatIcon name="add-comment-outline" size={22} />
|
||||
<List2 className="ml-[7px]">
|
||||
<List2 className="ml-[7px] text-black">
|
||||
{comment}
|
||||
</List2>
|
||||
</View>
|
||||
)}
|
||||
{showTaxon( taxon )}
|
||||
</View>
|
||||
<View className="flex-row">
|
||||
<View className="flex-row justify-evenly mx-3">
|
||||
{comment
|
||||
? (
|
||||
<Button
|
||||
text={t( "EDIT-COMMENT" )}
|
||||
onPress={openCommentBox}
|
||||
className="mx-3 grow"
|
||||
className="mx-2 grow"
|
||||
testID="ObsDetail.AgreeId.EditCommentButton"
|
||||
disabled={!comment}
|
||||
accessibilityHint={t( "Opens-add-comment-modal" )}
|
||||
@@ -110,9 +110,9 @@ const AgreeWithIDSheet = ( {
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
text={t( "COMMENT" )}
|
||||
text={t( "ADD-COMMENT" )}
|
||||
onPress={openCommentBox}
|
||||
className="mx-3 grow"
|
||||
className="mx-2 grow"
|
||||
testID="ObsDetail.AgreeId.commentButton"
|
||||
disabled={false}
|
||||
accessibilityHint={t( "Opens-add-comment-modal" )}
|
||||
@@ -122,10 +122,11 @@ const AgreeWithIDSheet = ( {
|
||||
<Button
|
||||
text={t( "AGREE" )}
|
||||
onPress={onAgree}
|
||||
className="mx-3 grow"
|
||||
className="mx-2 grow"
|
||||
testID="ObsDetail.AgreeId.cvSuggestionsButton"
|
||||
accessibilityRole="link"
|
||||
accessibilityHint={t( "Navigates-to-suggest-identification" )}
|
||||
level={comment && "primary"}
|
||||
/>
|
||||
</View>
|
||||
</BottomSheet>
|
||||
|
||||
@@ -8,7 +8,6 @@ import React from "react";
|
||||
import { useTheme } from "react-native-paper";
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
handleClose?: Function,
|
||||
black?: boolean,
|
||||
size?: number,
|
||||
@@ -18,7 +17,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const CloseButton = ( {
|
||||
className, handleClose, black, size, icon,
|
||||
handleClose, black, size, icon,
|
||||
width, height
|
||||
}: Props ): Node => {
|
||||
const navigation = useNavigation( );
|
||||
@@ -28,7 +27,6 @@ const CloseButton = ( {
|
||||
<INatIconButton
|
||||
icon={icon || "close"}
|
||||
size={size}
|
||||
className={className}
|
||||
color={black
|
||||
? theme.colors.tertiary
|
||||
: theme.colors.background}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import classNames from "classnames";
|
||||
import { Body1, Body3, Body4 } from "components/SharedComponents";
|
||||
import { Body1Bold, Body3, Body4 } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
@@ -61,9 +61,9 @@ const DisplayTaxonName = ( {
|
||||
|
||||
if ( !taxon ) {
|
||||
return (
|
||||
<Body1 className={textClass()} numberOfLines={1}>
|
||||
<Body1Bold className={textClass()} numberOfLines={1}>
|
||||
{t( "unknown" )}
|
||||
</Body1>
|
||||
</Body1Bold>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ const DisplayTaxonName = ( {
|
||||
: "";
|
||||
const text = piece + spaceChar;
|
||||
const TextComponent = scientificNameFirst || !commonName
|
||||
? Body1
|
||||
? Body1Bold
|
||||
: Body3;
|
||||
return (
|
||||
isItalics
|
||||
@@ -110,7 +110,7 @@ const DisplayTaxonName = ( {
|
||||
}
|
||||
|
||||
const TopTextComponent = !small
|
||||
? Body1
|
||||
? Body1Bold
|
||||
: Body3;
|
||||
const BottomTextComponent = !small
|
||||
? Body3
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import { useIsFocused } from "@react-navigation/native";
|
||||
import { Body3 } from "components/SharedComponents";
|
||||
import { Body3, Body3Bold } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import * as React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { useTheme } from "react-native-paper";
|
||||
import Svg, { ForeignObject, Path } from "react-native-svg";
|
||||
import { dropShadow } from "styles/global";
|
||||
@@ -34,6 +35,10 @@ const PhotoCount = ( { count, size, shadow }: Props ): React.Node => {
|
||||
photoCount = 99;
|
||||
}
|
||||
|
||||
const TextComponent = Platform.OS === "ios"
|
||||
? Body3
|
||||
: Body3Bold;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[{ height: size, width: size }, shadow && dropShadow]}
|
||||
@@ -59,10 +64,16 @@ const PhotoCount = ( { count, size, shadow }: Props ): React.Node => {
|
||||
clipRule="evenodd"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
<ForeignObject x="5%" y="26%" key={idx}>
|
||||
<Body3 className="text-center w-[16px]">
|
||||
<ForeignObject
|
||||
x="5%"
|
||||
y={Platform.OS === "ios"
|
||||
? "26%"
|
||||
: "20%"}
|
||||
key={idx}
|
||||
>
|
||||
<TextComponent className="text-center w-[16px]">
|
||||
{photoCount}
|
||||
</Body3>
|
||||
</TextComponent>
|
||||
</ForeignObject>
|
||||
</Svg>
|
||||
</View>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// @flow
|
||||
import { INatIcon } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import * as React from "react";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { TextInput, useTheme } from "react-native-paper";
|
||||
import { getShadowStyle } from "styles/global";
|
||||
@@ -20,7 +21,8 @@ type Props = {
|
||||
handleTextChange: Function,
|
||||
value: string,
|
||||
testID?: string,
|
||||
hasShadow?: boolean
|
||||
hasShadow?: boolean,
|
||||
input?: any
|
||||
}
|
||||
|
||||
// Ensure this component is placed outside of scroll views
|
||||
@@ -30,13 +32,15 @@ const SearchBar = ( {
|
||||
testID,
|
||||
handleTextChange,
|
||||
value,
|
||||
hasShadow
|
||||
}: Props ): React.Node => {
|
||||
hasShadow,
|
||||
input
|
||||
}: Props ): Node => {
|
||||
const theme = useTheme( );
|
||||
|
||||
return (
|
||||
<View className={containerClass}>
|
||||
<TextInput
|
||||
ref={input}
|
||||
accessibilityLabel="Search bar"
|
||||
keyboardType="default"
|
||||
mode="outlined"
|
||||
|
||||
13
src/components/SharedComponents/Typography/Body1Bold.js
Normal file
13
src/components/SharedComponents/Typography/Body1Bold.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
|
||||
import INatText from "./INatText";
|
||||
|
||||
const Body1Bold = ( props: any ): Node => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<INatText className="text-base font-medium text-darkGray" {...props} />
|
||||
);
|
||||
|
||||
export default Body1Bold;
|
||||
13
src/components/SharedComponents/Typography/Body3Bold.js
Normal file
13
src/components/SharedComponents/Typography/Body3Bold.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
|
||||
import INatText from "./INatText";
|
||||
|
||||
const Body3Bold = ( props: any ): Node => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<INatText className="text-sm font-medium text-darkGray" {...props} />
|
||||
);
|
||||
|
||||
export default Body3Bold;
|
||||
@@ -31,8 +31,10 @@ export { default as StickyToolbar } from "./StickyToolbar";
|
||||
export { default as Tabs } from "./Tabs/Tabs";
|
||||
export { default as TaxonResult } from "./TaxonResult";
|
||||
export { default as Body1 } from "./Typography/Body1";
|
||||
export { default as Body1Bold } from "./Typography/Body1Bold";
|
||||
export { default as Body2 } from "./Typography/Body2";
|
||||
export { default as Body3 } from "./Typography/Body3";
|
||||
export { default as Body3Bold } from "./Typography/Body3Bold";
|
||||
export { default as Body4 } from "./Typography/Body4";
|
||||
export { default as Heading1 } from "./Typography/Heading1";
|
||||
export { default as Heading2 } from "./Typography/Heading2";
|
||||
|
||||
@@ -1192,5 +1192,6 @@ SETTINGS = SETTINGS
|
||||
LOG-OUT = LOG OUT
|
||||
Log-in-to-iNaturalist = Log in to iNaturalist
|
||||
|
||||
Try-searching-for-a-location-name = Try searching for a location name to see the map
|
||||
Scan-the-area-around-you-for-organisms = Scan the area around you for organisms.
|
||||
Loading-iNaturalists-AR-Camera = Loading iNaturalist’s AR Camera
|
||||
Loading-iNaturalists-AR-Camera = Loading iNaturalist’s AR Camera
|
||||
|
||||
@@ -822,6 +822,7 @@
|
||||
"SETTINGS": "SETTINGS",
|
||||
"LOG-OUT": "LOG OUT",
|
||||
"Log-in-to-iNaturalist": "Log in to iNaturalist",
|
||||
"Try-searching-for-a-location-name": "Try searching for a location name to see the map",
|
||||
"Scan-the-area-around-you-for-organisms": "Scan the area around you for organisms.",
|
||||
"Loading-iNaturalists-AR-Camera": "Loading iNaturalist’s AR Camera"
|
||||
}
|
||||
|
||||
@@ -1192,5 +1192,6 @@ SETTINGS = SETTINGS
|
||||
LOG-OUT = LOG OUT
|
||||
Log-in-to-iNaturalist = Log in to iNaturalist
|
||||
|
||||
Try-searching-for-a-location-name = Try searching for a location name to see the map
|
||||
Scan-the-area-around-you-for-organisms = Scan the area around you for organisms.
|
||||
Loading-iNaturalists-AR-Camera = Loading iNaturalist’s AR Camera
|
||||
Loading-iNaturalists-AR-Camera = Loading iNaturalist’s AR Camera
|
||||
|
||||
@@ -4,7 +4,7 @@ import AddID from "components/AddID/AddID";
|
||||
import CameraContainer from "components/Camera/CameraContainer";
|
||||
import Explore from "components/Explore/Explore";
|
||||
import Identify from "components/Identify/Identify";
|
||||
import LocationPicker from "components/LocationPicker/LocationPicker";
|
||||
import LocationPickerContainer from "components/LocationPicker/LocationPickerContainer";
|
||||
import ForgotPassword from "components/LoginSignUp/ForgotPassword";
|
||||
import LicensePhotos from "components/LoginSignUp/LicensePhotos";
|
||||
import Login from "components/LoginSignUp/Login";
|
||||
@@ -336,11 +336,8 @@ const BottomTabs = ( ) => {
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="LocationPicker"
|
||||
component={LocationPicker}
|
||||
options={{
|
||||
...blankHeaderTitle,
|
||||
...hideHeaderLeft
|
||||
}}
|
||||
component={LocationPickerContainer}
|
||||
options={hideHeader}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="MediaViewer"
|
||||
|
||||
@@ -1,49 +1,20 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react-native";
|
||||
import { fireEvent, screen } from "@testing-library/react-native";
|
||||
import LocationPicker from "components/LocationPicker/LocationPicker";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import ObsEditProvider from "providers/ObsEditProvider";
|
||||
import React from "react";
|
||||
|
||||
import factory from "../../../factory";
|
||||
import { renderComponent } from "../../../helpers/render";
|
||||
|
||||
jest.mock( "@react-navigation/native", ( ) => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
return {
|
||||
...actualNav,
|
||||
useNavigation: ( ) => ( {
|
||||
setOptions: jest.fn( )
|
||||
} )
|
||||
};
|
||||
} );
|
||||
|
||||
// Mock ObservationProvider so it provides a specific array of observations
|
||||
// without any current observation or ability to update or fetch
|
||||
// observations
|
||||
jest.mock( "providers/ObsEditProvider" );
|
||||
const mockObsEditProviderWithObs = obs => ObsEditProvider.mockImplementation( ( { children } ) => (
|
||||
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
||||
<ObsEditContext.Provider value={{
|
||||
observations: obs,
|
||||
currentObservation: obs[0]
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ObsEditContext.Provider>
|
||||
) );
|
||||
|
||||
const renderLocationPicker = ( ) => renderComponent(
|
||||
<ObsEditProvider>
|
||||
<LocationPicker route={{
|
||||
params: {
|
||||
goBackOnSave: jest.fn( )
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ObsEditProvider>
|
||||
);
|
||||
const observations = [
|
||||
factory( "RemoteObservation", {
|
||||
// Oakland, CA latlng
|
||||
latitude: 37.804855,
|
||||
longitude: -122.272504
|
||||
} )
|
||||
];
|
||||
|
||||
const mockPlaceResult = factory( "RemotePlace", {
|
||||
display_name: "New York",
|
||||
@@ -62,6 +33,31 @@ jest.mock( "sharedHooks/useAuthenticatedQuery", ( ) => ( {
|
||||
} )
|
||||
} ) );
|
||||
|
||||
const mockSelectPlaceResult = jest.fn( );
|
||||
const mockRegion = {
|
||||
latitude: observations[0].latitude,
|
||||
longitude: observations[0].longitude,
|
||||
latitudeDelta: 0.2,
|
||||
longitudeDelta: 0.2
|
||||
};
|
||||
|
||||
const renderLocationPicker = region => renderComponent(
|
||||
<ObsEditContext.Provider value={{
|
||||
updateObservationKeys: jest.fn( )
|
||||
}}
|
||||
>
|
||||
<LocationPicker
|
||||
region={region}
|
||||
locationName="Oakland, CA"
|
||||
updateLocationName={location => jest.fn( location )}
|
||||
hidePlaceResults={false}
|
||||
selectPlaceResult={mockSelectPlaceResult}
|
||||
mapType="standard"
|
||||
loading={false}
|
||||
/>
|
||||
</ObsEditContext.Provider>
|
||||
);
|
||||
|
||||
describe( "LocationPicker", () => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
@@ -70,67 +66,42 @@ describe( "LocationPicker", () => {
|
||||
it(
|
||||
"should display latitude corresponding with location name",
|
||||
async ( ) => {
|
||||
const observations = [
|
||||
factory( "RemoteObservation", {
|
||||
// Oakland, CA latlng
|
||||
latitude: 37.804855,
|
||||
longitude: -122.272504
|
||||
} )
|
||||
];
|
||||
mockObsEditProviderWithObs( observations );
|
||||
renderLocationPicker( );
|
||||
const initialLatitude = screen.getByText( new RegExp( observations[0].latitude ) );
|
||||
expect( initialLatitude ).toBeTruthy( );
|
||||
renderLocationPicker( mockRegion );
|
||||
await screen.findByText( new RegExp( observations[0].latitude ) );
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
"should show search results when a user changes search text",
|
||||
async ( ) => {
|
||||
const observations = [
|
||||
factory( "RemoteObservation", {
|
||||
// Oakland, CA latlng
|
||||
latitude: 37.804855,
|
||||
longitude: -122.272504
|
||||
} )
|
||||
];
|
||||
mockObsEditProviderWithObs( observations );
|
||||
renderLocationPicker( );
|
||||
renderLocationPicker( mockRegion );
|
||||
const input = screen.getByTestId( "LocationPicker.locationSearch" );
|
||||
const initialLatitude = screen.getByText( new RegExp( observations[0].latitude ) );
|
||||
expect( initialLatitude ).toBeTruthy( );
|
||||
expect( input ).toBeVisible( );
|
||||
await screen.findByText( new RegExp( observations[0].latitude ) );
|
||||
fireEvent.changeText( input, "New" );
|
||||
await waitFor( ( ) => {
|
||||
expect( screen.getByText( mockPlaceResult.display_name ) ).toBeTruthy( );
|
||||
} );
|
||||
await screen.findByText( mockPlaceResult.display_name );
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
"should move map to new coordinates when a user presses place result",
|
||||
"should update map with new place results when a user taps a place in dropdown",
|
||||
async ( ) => {
|
||||
const observations = [
|
||||
factory( "RemoteObservation", {
|
||||
// Oakland, CA latlng
|
||||
latitude: 37.804855,
|
||||
longitude: -122.272504
|
||||
} )
|
||||
];
|
||||
mockObsEditProviderWithObs( observations );
|
||||
renderLocationPicker( );
|
||||
renderLocationPicker( mockRegion );
|
||||
const input = screen.getByTestId( "LocationPicker.locationSearch" );
|
||||
const initialLatitude = screen.getByText( new RegExp( observations[0].latitude ) );
|
||||
expect( initialLatitude ).toBeTruthy( );
|
||||
await screen.findByText( new RegExp( observations[0].latitude ) );
|
||||
fireEvent.changeText( input, "New" );
|
||||
await waitFor( ( ) => {
|
||||
expect( screen.getByText( mockPlaceResult.display_name ) ).toBeTruthy( );
|
||||
} );
|
||||
fireEvent.press( screen.getByText( mockPlaceResult.display_name ) );
|
||||
await waitFor( ( ) => {
|
||||
expect( screen.getByText(
|
||||
new RegExp( mockPlaceResult.point_geojson.coordinates[0] )
|
||||
) ).toBeTruthy( );
|
||||
const placeResult = screen.getByText( mockPlaceResult.display_name );
|
||||
expect( placeResult ).toBeVisible( );
|
||||
fireEvent.press( placeResult );
|
||||
expect( mockSelectPlaceResult ).toHaveBeenCalledTimes( 1 );
|
||||
renderLocationPicker( {
|
||||
...mockRegion,
|
||||
latitude: mockPlaceResult.point_geojson.coordinates[1],
|
||||
longitude: mockPlaceResult.point_geojson.coordinates[0]
|
||||
} );
|
||||
await screen.findByText(
|
||||
new RegExp( mockPlaceResult.point_geojson.coordinates[0] )
|
||||
);
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
Reference in New Issue
Block a user