Files
iNaturalistReactNative/src/sharedHooks/useWatchPosition.ts
Ryan Stelly b78be9243d lint rule & autofix for "trailing comma" (#3299)
* (lint) MOB-1063 enforce trailing commas

* autofix trailing commas

* manually fix newly introduced maxlen violations

* add trailing comma convention to i18n build
2025-12-22 20:17:13 -06:00

134 lines
4.0 KiB
TypeScript

import type {
GeolocationError,
GeolocationResponse,
} from "@react-native-community/geolocation";
import { useNavigation } from "@react-navigation/native";
import { useCallback, useEffect, useState } from "react";
// Please don't change this to an aliased path or the e2e mock will not get
// used in our e2e tests on Github Actions
import { clearWatch, watchPosition } from "../sharedHelpers/geolocationWrapper";
export const TARGET_POSITIONAL_ACCURACY = 10;
const MAX_POSITION_AGE_MS = 60_000;
export interface UserLocation {
latitude: number;
longitude: number;
positional_accuracy: number;
altitude: number | null;
altitudinal_accuracy: number | null;
}
const geolocationOptions = {
distanceFilter: 0,
enableHighAccuracy: true,
maximumAge: 0,
};
const useWatchPosition = ( options: {
shouldFetchLocation: boolean;
} ) => {
const navigation = useNavigation( );
const [currentPosition, setCurrentPosition] = useState<GeolocationResponse | null>( null );
const [subscriptionId, setSubscriptionId] = useState<number | null>( null );
const [userLocation, setUserLocation] = useState<UserLocation | null>( null );
const { shouldFetchLocation } = options;
const [hasFocus, setHasFocus] = useState( true );
const shouldStartWatch = shouldFetchLocation
&& subscriptionId === null
&& hasFocus
&& ( !userLocation || userLocation.positional_accuracy >= TARGET_POSITIONAL_ACCURACY );
const shouldStopWatch = subscriptionId !== null
&& currentPosition?.coords?.accuracy < TARGET_POSITIONAL_ACCURACY;
const stopWatch = useCallback( ( id: number ) => {
clearWatch( id );
setSubscriptionId( null );
setCurrentPosition( null );
}, [] );
const startWatch = useCallback( ( ) => {
const success = ( position: GeolocationResponse ) => {
const age = Date.now() - position.timestamp;
if ( age > MAX_POSITION_AGE_MS ) return;
setCurrentPosition( position );
};
const failure = ( error: GeolocationError ) => {
console.warn( "useWatchPosition error: ", error );
if ( subscriptionId ) {
stopWatch( subscriptionId );
}
};
try {
const watchID = watchPosition(
success,
failure,
geolocationOptions,
);
if ( typeof ( watchID ) !== "number" ) {
throw new Error( "watchPosition failed to return a watchID" );
}
setSubscriptionId( watchID );
} catch ( error ) {
failure( error as GeolocationError );
}
}, [stopWatch, subscriptionId] );
useEffect( ( ) => {
if ( !currentPosition ) { return; }
const newLocation = {
latitude: currentPosition?.coords?.latitude,
longitude: currentPosition?.coords?.longitude,
positional_accuracy: currentPosition?.coords?.accuracy,
altitude: currentPosition?.coords?.altitude,
altitudinal_accuracy: currentPosition?.coords?.altitudeAccuracy,
};
setUserLocation( newLocation );
if ( shouldStopWatch ) {
stopWatch( subscriptionId );
}
}, [currentPosition, stopWatch, subscriptionId, shouldStopWatch] );
useEffect( ( ) => {
if ( shouldStartWatch ) {
startWatch( );
}
}, [shouldStartWatch, startWatch] );
useEffect( ( ) => {
// When we leave the screen this hook was used on...
const unsubscribe = navigation.addListener( "blur", ( ) => {
// ...stop watching for location updates if we were...
if ( subscriptionId !== null ) {
stopWatch( subscriptionId );
}
// ...and wipe the current location so we don't pick up a stale one later
setUserLocation( null );
setHasFocus( false );
} );
return unsubscribe;
}, [navigation, stopWatch, subscriptionId] );
// Listen for focus. We only want to fetch location when this screen has focus.
useEffect( ( ) => {
const unsubscribe = navigation.addListener( "focus", ( ) => {
setHasFocus( true );
} );
return unsubscribe;
}, [navigation] );
return {
isFetchingLocation: subscriptionId !== null,
stopWatch,
subscriptionId,
userLocation,
};
};
export default useWatchPosition;