diff --git a/client/app/_layout.tsx b/client/app/_layout.tsx index e79865c..87d2877 100644 --- a/client/app/_layout.tsx +++ b/client/app/_layout.tsx @@ -12,8 +12,8 @@ import { useReactQueryDevTools } from '@dev-plugins/react-query' import { useColorScheme } from '@/hooks/useColorScheme' import store from '@/utils/store' import { useStartBackgroundFetchServiceEffect } from '@/utils/background' -import { useStartGeolocationServiceEffect } from '@/utils/geolocation' import LoginProvider from '@/components/LoginProvider' +import GeolocationProvider from '@/components/GeolocationProvider' const queryClient = new QueryClient({ defaultOptions: { @@ -26,7 +26,6 @@ const queryClient = new QueryClient({ }) export default function RootLayout() { - useStartGeolocationServiceEffect() useStartBackgroundFetchServiceEffect() const colorScheme = useColorScheme() @@ -45,16 +44,18 @@ export default function RootLayout() { persistOptions={{ persister: asyncStoragePersister }} onSuccess={() => queryClient.resumePausedMutations().then(() => queryClient.invalidateQueries())}> - - - - - - - - - - + + + + + + + + + + + + diff --git a/client/components/GeolocationProvider.tsx b/client/components/GeolocationProvider.tsx new file mode 100644 index 0000000..a5a89ca --- /dev/null +++ b/client/components/GeolocationProvider.tsx @@ -0,0 +1,32 @@ +import { ReactNode, useEffect } from 'react' +import { useAuth } from '@/hooks/useAuth' +import { useQueuedLocations, useUnqueueLocation } from '@/hooks/useLocation' +import { useGeolocationMutation } from '@/hooks/mutations/useGeolocationMutation' +import { useStartGeolocationServiceEffect } from '@/utils/geolocation' + +export default function GeolocationProvider({ children }: { children: ReactNode }) { + useStartGeolocationServiceEffect() + + const auth = useAuth() + const geolocationsQueue = useQueuedLocations() + const unqueueLocation = useUnqueueLocation() + const geolocationMutation = useGeolocationMutation({ + auth, + onPostSuccess: ({ data, variables: location }) => { + unqueueLocation(location) + geolocationMutation.reset() + }, + onError: ({ response, error }) => { console.error(response, error) } + }) + + useEffect(() => { + if (geolocationsQueue.length === 0 || geolocationMutation.isPending) + return + const locToSend = geolocationsQueue[0] + geolocationMutation.mutate(locToSend) + }, [auth, geolocationsQueue]) + + return <> + {children} + +} \ No newline at end of file diff --git a/client/components/Map.tsx b/client/components/Map.tsx index a598b91..7278efb 100644 --- a/client/components/Map.tsx +++ b/client/components/Map.tsx @@ -2,10 +2,10 @@ import { StyleSheet } from 'react-native' import MapLibreGL, { Camera, FillLayer, LineLayer, MapView, PointAnnotation, RasterLayer, RasterSource, ShapeSource, UserLocation } from '@maplibre/maplibre-react-native' import { FontAwesome5 } from '@expo/vector-icons' import { circle } from '@turf/circle' -import { useLocation } from '@/hooks/useLocation' +import { useLastOwnLocation } from '@/hooks/useLocation' export default function Map() { - const userLocation = useLocation() + const userLocation = useLastOwnLocation() MapLibreGL.setAccessToken(null) const accuracyCircle = circle([userLocation?.coords.longitude ?? 0, userLocation?.coords.latitude ?? 0], userLocation?.coords.accuracy ?? 0, {steps: 64, units: 'meters'}) return ( diff --git a/client/components/Map.web.tsx b/client/components/Map.web.tsx index 1be6dd6..eb4ea1b 100644 --- a/client/components/Map.web.tsx +++ b/client/components/Map.web.tsx @@ -1,4 +1,4 @@ -import { useLocation } from "@/hooks/useLocation" +import { useLastOwnLocation } from "@/hooks/useLocation" import { circle } from "@turf/circle" import { type Map as MaplibreGLMap } from "maplibre-gl" import { RLayer, RMap, RMarker, RNavigationControl, RSource, useMap } from "maplibre-react-components" @@ -21,7 +21,7 @@ export default function Map() { } function UserLocation() { - const userLocation = useLocation() + const userLocation = useLastOwnLocation() const [firstUserPositionFetched, setFirstUserPositionFetched] = useState(false) const map: MaplibreGLMap = useMap() if (userLocation != null && !firstUserPositionFetched) { diff --git a/client/constants/Constants.ts b/client/constants/Constants.ts new file mode 100644 index 0000000..5e2e0de --- /dev/null +++ b/client/constants/Constants.ts @@ -0,0 +1,3 @@ +export const Constants = { + MIN_DELAY_LOCATION_SENT: 20 +} diff --git a/client/hooks/mutations/useGeolocationMutation.ts b/client/hooks/mutations/useGeolocationMutation.ts new file mode 100644 index 0000000..761f3a0 --- /dev/null +++ b/client/hooks/mutations/useGeolocationMutation.ts @@ -0,0 +1,56 @@ +import { AuthState } from "@/utils/features/location/authSlice" +import { useMutation } from "@tanstack/react-query" +import { LocationObject } from "expo-location" + +type ErrorResponse = { + error: string + message: string + statusCode: number +} + +type LoginForm = { + name: string + password: string +} + +type onPostSuccessFunc = (data: any, variables: LocationObject, context: unknown) => void +type ErrorFuncProps = { response?: ErrorResponse, error?: Error } +type onErrorFunc = (props: ErrorFuncProps) => void + +type PostProps = { + auth: AuthState + onPostSuccess?: onPostSuccessFunc + onError?: onErrorFunc +} + +export const useGeolocationMutation = ({ auth, onPostSuccess, onError }: PostProps) => { + return useMutation({ + mutationFn: async (location: LocationObject) => { + return fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/geolocations/`, { + method: "POST", + headers: { + "Authorization": `Bearer ${auth.token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + longitude: location.coords.longitude, + latitude: location.coords.latitude, + speed: location.coords.speed, + accuracy: location.coords.accuracy, + altitude: location.coords.altitude, + altitudeAccuracy: location.coords.altitudeAccuracy, + timestamp: location.timestamp, + }) + }).then(resp => resp.json()) + }, + networkMode: 'offlineFirst', + onSuccess: async (data, location: LocationObject, context: unknown) => { + if (onPostSuccess) + onPostSuccess(data, location, context) + }, + onError: async (error: Error) => { + if (onError) + onError({ error: error }) + } + }) +} diff --git a/client/hooks/useLocation.ts b/client/hooks/useLocation.ts index 856b708..eb4efb7 100644 --- a/client/hooks/useLocation.ts +++ b/client/hooks/useLocation.ts @@ -1,9 +1,15 @@ import { LocationObject } from "expo-location" import { useAppDispatch, useAppSelector } from "./useStore" -import { setLocation } from "@/utils/features/location/locationSlice" +import { setLastLocation, unqueueLocation } from "@/utils/features/location/locationSlice" -export const useLocation = () => useAppSelector((state) => state.location.location) -export const useSetLocation = () => { +export const useLastOwnLocation = () => useAppSelector((state) => state.location.lastOwnLocation) +export const useQueuedLocations = () => useAppSelector((state) => state.location.queuedLocations) + +export const useSetLastLocation = () => { const dispatch = useAppDispatch() - return (location: LocationObject) => dispatch(setLocation(location)) + return (location: LocationObject) => dispatch(setLastLocation(location)) +} +export const useUnqueueLocation = () => { + const dispatch = useAppDispatch() + return (location: LocationObject) => dispatch(unqueueLocation(location)) } diff --git a/client/utils/features/location/authSlice.ts b/client/utils/features/location/authSlice.ts index 88c9c5e..7192b21 100644 --- a/client/utils/features/location/authSlice.ts +++ b/client/utils/features/location/authSlice.ts @@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import * as SecureStore from '@/utils/SecureStore' import { Platform } from 'react-native' -interface AuthState { +export interface AuthState { loggedIn: boolean, name: string | null, token: string | null, diff --git a/client/utils/features/location/locationSlice.ts b/client/utils/features/location/locationSlice.ts index ed606a4..c7c808d 100644 --- a/client/utils/features/location/locationSlice.ts +++ b/client/utils/features/location/locationSlice.ts @@ -1,24 +1,37 @@ +import { Constants } from '@/constants/Constants' import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { LocationObject } from 'expo-location' interface LocationState { - location: LocationObject | null + lastOwnLocation: LocationObject | null + lastSentLocation: LocationObject | null + queuedLocations: LocationObject[] } const initialState: LocationState = { - location: null + lastOwnLocation: null, + lastSentLocation: null, + queuedLocations: [] } export const locationSlice = createSlice({ name: 'location', initialState: initialState, reducers: { - setLocation: (state, action: PayloadAction) => { - state.location = action.payload + setLastLocation: (state, action: PayloadAction) => { + const location: LocationObject = action.payload + state.lastOwnLocation = location + if (state.lastSentLocation === null || (location.timestamp - state.lastSentLocation.timestamp) >= Constants.MIN_DELAY_LOCATION_SENT * 1000) { + state.lastSentLocation = location + state.queuedLocations.push(location) + } + }, + unqueueLocation: (state, action: PayloadAction) => { + state.queuedLocations.pop() }, }, }) -export const { setLocation } = locationSlice.actions +export const { setLastLocation, unqueueLocation } = locationSlice.actions export default locationSlice.reducer diff --git a/client/utils/geolocation.ts b/client/utils/geolocation.ts index f7c6a62..bf95cd4 100644 --- a/client/utils/geolocation.ts +++ b/client/utils/geolocation.ts @@ -1,7 +1,7 @@ import * as Location from 'expo-location' import * as TaskManager from 'expo-task-manager' import { Platform } from 'react-native' -import { setLocation } from './features/location/locationSlice' +import { setLastLocation } from './features/location/locationSlice' import store from './store' import { useEffect } from 'react' @@ -13,10 +13,7 @@ TaskManager.defineTask(LOCATION_TASK, async ({ data, error }: any) => { return } const { locations } = data - store.dispatch(setLocation(locations.at(-1))) - for (let location of locations) { - // TODO Envoyer les positions au serveur - } + store.dispatch(setLastLocation(locations.at(-1))) }) export async function startGeolocationService(): Promise void)> { @@ -48,7 +45,7 @@ export async function startGeolocationService(): Promise void)> { return async () => await Location.stopLocationUpdatesAsync(LOCATION_TASK) } else { - const locationSubscription = await Location.watchPositionAsync({accuracy: Location.Accuracy.BestForNavigation}, location_nouveau => store.dispatch(setLocation(location_nouveau))) + const locationSubscription = await Location.watchPositionAsync({accuracy: Location.Accuracy.BestForNavigation}, location_nouveau => store.dispatch(setLastLocation(location_nouveau))) return locationSubscription.remove } }