Fix photos + API hooks + cleaning

This commit is contained in:
Sebastien Riviere
2026-02-17 14:32:37 +01:00
parent e0aaeb63f7
commit 05a60612c6
24 changed files with 341 additions and 231 deletions

3
.gitignore vendored
View File

@@ -137,3 +137,6 @@ yarn.lock
keys/ keys/
temp.* temp.*
temp/ temp/
.env
.env.development
.env.production

View File

@@ -10,7 +10,8 @@ Problèmes
[ ] Une équipe perdait sans arrêt la connection avec le serveur [ ] Une équipe perdait sans arrêt la connection avec le serveur
[ ] La position en arrière plan, téléphone éteint par exemple, n'avait pas l'air de fonctionner (une équipe n'avait pas de notif) [ ] La position en arrière plan, téléphone éteint par exemple, n'avait pas l'air de fonctionner (une équipe n'avait pas de notif)
[ ] La photo d'une des équipes ne parvenait pas jusqu'au serveur. Tout semblait normal pour l'équipe. [x] La photo d'une des équipes ne parvenait pas jusqu'au serveur. Tout semblait normal pour l'équipe. (Potentiellement un problème
de conversion : 046512 -> 46512)
[x] Le focus sur une équipe dans la page principale des admins est à revoir. Par exemple, le zoom seul devrait désactiver le focus [x] Le focus sur une équipe dans la page principale des admins est à revoir. Par exemple, le zoom seul devrait désactiver le focus
automatique. automatique.
[x] Il est pas évident de voir qu'elles équipes sont encore en jeu sur la page principale admin. Notamment, on voudrait que les [x] Il est pas évident de voir qu'elles équipes sont encore en jeu sur la page principale admin. Notamment, on voudrait que les

View File

@@ -7,12 +7,30 @@ lang: en-GB
This tutorial will help you to set up your development environment, use a dev build and create an APK. This tutorial will help you to set up your development environment, use a dev build and create an APK.
## Table of Contents ## Table of Contents
* [Environment](#environment) : Dependencies, packages, app key and device * [Environment](#environment) : Dependencies, packages, app key, device and .env
* [Dev build](#dev-build) : Create, install and use * [Dev build](#dev-build) : Create, install and use
* [APK](#apk) : Create and install * [APK](#apk) : Create and install
## Environment ## Environment
### .env files
Some infos like API keys or IP addresses cannot be pushed on the public repository, therefore you have to create the .env files that will store those values. Go in the `traque-app` folder, create those two files and replace the FILL_HERE with the correct values (you can ask someone already working on the project) :
* `.env.development` :
```.env
EXPO_PUBLIC_SERVER_URL=FILL_HERE
EXPO_PUBLIC_SOCKET_URL=FILL_HERE
```
* `.env.production` :
```.env
EXPO_PUBLIC_SERVER_URL=FILL_HERE
EXPO_PUBLIC_SOCKET_URL=FILL_HERE
```
### Installing dependencies and preparing the device ### Installing dependencies and preparing the device
You will need to install Android Studio, some SDKs and prepare your device if you want to use a physical one. Follow this [tutorial](https://reactnative.dev/docs/set-up-your-environment?os=linux&platform=android). You will need to install Android Studio, some SDKs and prepare your device if you want to use a physical one. Follow this [tutorial](https://reactnative.dev/docs/set-up-your-environment?os=linux&platform=android).

View File

@@ -8,22 +8,22 @@ import { CustomButton } from '../components/button';
import { CustomImage } from '../components/image'; import { CustomImage } from '../components/image';
import { CustomTextInput } from '../components/input'; import { CustomTextInput } from '../components/input';
// Contexts // Contexts
import { useSocket } from "../context/socketContext";
import { useTeamConnexion } from "../context/teamConnexionContext"; import { useTeamConnexion } from "../context/teamConnexionContext";
import { useTeamContext } from "../context/teamContext"; import { useTeamContext } from "../context/teamContext";
// Hooks // Hooks
import { usePickImage } from '../hook/usePickImage'; import { usePickImage } from '../hook/usePickImage';
import { useImageApi } from '../hook/useImageApi';
// Util // Util
import { Colors } from '../util/colors'; import { Colors } from '../util/colors';
const Index = () => { const Index = () => {
const router = useRouter(); const router = useRouter();
const {SERVER_URL} = useSocket(); const {login, loggedIn} = useTeamConnexion();
const {login, loggedIn, loading} = useTeamConnexion();
const {getLocationAuthorization, stopLocationTracking} = useTeamContext(); const {getLocationAuthorization, stopLocationTracking} = useTeamContext();
const {image, pickImage, sendImage} = usePickImage(); const {image, pickImage} = usePickImage();
const [teamID, setTeamID] = useState(""); const [teamId, setTeamId] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const { uploadTeamImage } = useImageApi();
// Disbaling location tracking // Disbaling location tracking
useEffect(() => { useEffect(() => {
@@ -32,32 +32,37 @@ const Index = () => {
// Routeur // Routeur
useEffect(() => { useEffect(() => {
if (!loading && loggedIn) { if (loggedIn) {
uploadTeamImage(image?.uri);
router.replace("/interface"); router.replace("/interface");
} }
}, [router, loggedIn, loading]); }, [router, loggedIn, uploadTeamImage, image]);
const handleSubmit = async () => {
if (isSubmitting || !getLocationAuthorization()) return;
function handleSubmit() {
if (!isSubmitting && !loading) {
setIsSubmitting(true); setIsSubmitting(true);
if (getLocationAuthorization()) {
login(parseInt(teamID)) const regex = /^\d{6}$/;
.then((response) => { if (!regex.test(teamId)) {
setTimeout(() => Alert.alert("Erreur", "Veuillez entrer un ID d'équipe valide."), 100);
return;
}
try {
const response = await login(teamId);
if (response.isLoggedIn) { if (response.isLoggedIn) {
sendImage(`${SERVER_URL}/upload?team=${teamID}`); setTeamId("");
} else { } else {
Alert.alert("Échec", "L'ID d'équipe est inconnu."); setTimeout(() => Alert.alert("Échec", "L'ID d'équipe est inconnu."), 100);
} }
} catch (error) {
setTimeout(() => Alert.alert("Échec", "La connexion au serveur a échoué."), 100);
} finally {
setIsSubmitting(false); setIsSubmitting(false);
})
.catch(() => {
Alert.alert("Échec", "La connection au serveur a échoué.");
setIsSubmitting(false);
});
}
setTeamID("");
}
} }
};
return ( return (
<ScrollView contentContainerStyle={styles.container}> <ScrollView contentContainerStyle={styles.container}>
@@ -67,7 +72,7 @@ const Index = () => {
<Text style={styles.logoText}>LA TRAQUE</Text> <Text style={styles.logoText}>LA TRAQUE</Text>
</View> </View>
<View style={styles.subContainer}> <View style={styles.subContainer}>
<CustomTextInput value={teamID} inputMode="numeric" placeholder="ID de l'équipe" style={styles.input} onChangeText={setTeamID}/> <CustomTextInput value={teamId} inputMode="numeric" placeholder="ID de l'équipe" style={styles.input} onChangeText={setTeamId}/>
</View> </View>
<View style={styles.subContainer}> <View style={styles.subContainer}>
<Text style={{fontSize: 15}}>Appuyer pour changer la photo d&apos;équipe</Text> <Text style={{fontSize: 15}}>Appuyer pour changer la photo d&apos;équipe</Text>
@@ -75,7 +80,7 @@ const Index = () => {
<CustomImage source={image ? {uri: image.uri} : require('../assets/images/missing_image.jpg')} onPress={pickImage}/> <CustomImage source={image ? {uri: image.uri} : require('../assets/images/missing_image.jpg')} onPress={pickImage}/>
</View> </View>
<View style={styles.subContainer}> <View style={styles.subContainer}>
<CustomButton label={(isSubmitting || loading) ? "..." : "Valider"} onPress={handleSubmit}/> <CustomButton label={isSubmitting ? "..." : "Valider"} onPress={handleSubmit}/>
</View> </View>
</View> </View>
</ScrollView> </ScrollView>

View File

@@ -20,9 +20,10 @@ import { Colors } from '../util/colors';
const Interface = () => { const Interface = () => {
const router = useRouter(); const router = useRouter();
const {messages, nextZoneDate, isShrinking, startLocationTracking, stopLocationTracking, gameState} = useTeamContext(); const {teamInfos, messages, nextZoneDate, isShrinking, startLocationTracking, stopLocationTracking, gameState} = useTeamContext();
const {loggedIn, logout, loading} = useTeamConnexion(); const {name, ready, captured, locationSendDeadline, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = teamInfos;
const {name, ready, captured, locationSendDeadline, sendCurrentPosition, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = useGame(); const {loggedIn, logout} = useTeamConnexion();
const {sendCurrentPosition} = useGame();
const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000); const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000);
const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000); const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000);
const [timeLeftOutOfZone] = useTimeDifference(outOfZoneDeadline, 1000); const [timeLeftOutOfZone] = useTimeDifference(outOfZoneDeadline, 1000);
@@ -48,12 +49,10 @@ const Interface = () => {
// Router // Router
useEffect(() => { useEffect(() => {
if (!loading) {
if (!loggedIn) { if (!loggedIn) {
router.replace("/"); router.replace("/");
} }
} }, [router, loggedIn]);
}, [router, loggedIn, loading]);
// Activating geolocation tracking // Activating geolocation tracking
useEffect(() => { useEffect(() => {

View File

@@ -9,7 +9,6 @@ import { CustomTextInput } from './input';
import { Stat } from './stat'; import { Stat } from './stat';
// Contexts // Contexts
import { useTeamContext } from '../context/teamContext'; import { useTeamContext } from '../context/teamContext';
import { useSocket } from '../context/socketContext';
// Hooks // Hooks
import { useTimeDifference } from '../hook/useTimeDifference'; import { useTimeDifference } from '../hook/useTimeDifference';
import { useGame } from '../hook/useGame'; import { useGame } from '../hook/useGame';
@@ -17,17 +16,18 @@ import { useGame } from '../hook/useGame';
import { GameState } from '../util/gameState'; import { GameState } from '../util/gameState';
import { Colors } from '../util/colors'; import { Colors } from '../util/colors';
import { secondsToHHMMSS } from '../util/functions'; import { secondsToHHMMSS } from '../util/functions';
import { useImageApi } from '../hook/useImageApi';
export const Drawer = ({ height }) => { export const Drawer = ({ height }) => {
const [collapsibleState, setCollapsibleState] = useState(true); const [collapsibleState, setCollapsibleState] = useState(true);
const [enemyCaptureCode, setEnemyCaptureCode] = useState(""); const [enemyCaptureCode, setEnemyCaptureCode] = useState("");
const {SERVER_URL} = useSocket(); const {teamInfos, gameState, startDate} = useTeamContext();
const {gameState, startDate} = useTeamContext(); const {enemyName, captureCode, name, distance, finishDate, nCaptures, nSentLocation, hasHandicap} = teamInfos;
const {capture, enemyName, captureCode, name, teamId, distance, finishDate, nCaptures, nSentLocation, hasHandicap} = useGame(); const {capture} = useGame();
const [timeSinceStart] = useTimeDifference(startDate, 1000); const [timeSinceStart] = useTimeDifference(startDate, 1000);
const [enemyImageURI, setEnemyImageURI] = useState("../assets/images/missing_image.jpg");
const [captureStatus, setCaptureStatus] = useState(0); // 0 : no capture | 1 : waiting for response from server | 2 : capture failed | 3 : capture succesful const [captureStatus, setCaptureStatus] = useState(0); // 0 : no capture | 1 : waiting for response from server | 2 : capture failed | 3 : capture succesful
const captureStatusColor = {0: "#777", 1: "#FFA500", 2: "#FF6B6B", 3: "#81C784"}; const captureStatusColor = {0: "#777", 1: "#FFA500", 2: "#FF6B6B", 3: "#81C784"};
const { enemyImage } = useImageApi();
const avgSpeed = useMemo(() => { const avgSpeed = useMemo(() => {
const hours = (finishDate ? (finishDate - startDate) : timeSinceStart*1000) / 1000 / 3600; const hours = (finishDate ? (finishDate - startDate) : timeSinceStart*1000) / 1000 / 3600;
@@ -48,11 +48,6 @@ export const Drawer = ({ height }) => {
} }
}, [captureStatus]); }, [captureStatus]);
// Refresh the image
useEffect(() => {
setEnemyImageURI(`${SERVER_URL}/photo/enemy?team=${teamId}&t=${new Date().getTime()}`);
}, [SERVER_URL, enemyName, teamId]);
const handleCapture = () => { const handleCapture = () => {
if (captureStatus != 1) { if (captureStatus != 1) {
setCaptureStatus(1); setCaptureStatus(1);
@@ -86,8 +81,8 @@ export const Drawer = ({ height }) => {
} }
{ gameState == GameState.PLAYING && !hasHandicap && <Fragment> { gameState == GameState.PLAYING && !hasHandicap && <Fragment>
<View style={styles.imageContainer}> <View style={styles.imageContainer}>
{<Text style={{fontSize: 15, margin: 5}}>{"Cible (" + (enemyName ?? "Indisponible") + ")"}</Text>} <Text style={{fontSize: 15, margin: 5}}>{"Cible (" + (enemyName ?? "Indisponible") + ")"}</Text>
{<CustomImage source={{ uri : enemyImageURI }} canZoom/>} <CustomImage source={enemyImage} canZoom/>
</View> </View>
<View style={styles.actionsContainer}> <View style={styles.actionsContainer}>
<View style={styles.actionsLeftContainer}> <View style={styles.actionsLeftContainer}>

View File

@@ -1,3 +1,4 @@
// React
import { Fragment } from 'react'; import { Fragment } from 'react';
import { Polygon } from 'react-native-maps'; import { Polygon } from 'react-native-maps';
import { circleToPolygon } from '../util/functions'; import { circleToPolygon } from '../util/functions';

View File

@@ -7,15 +7,13 @@ import LinearGradient from 'react-native-linear-gradient';
import { DashedCircle, InvertedCircle, InvertedPolygon } from './layer'; import { DashedCircle, InvertedCircle, InvertedPolygon } from './layer';
// Contexts // Contexts
import { useTeamContext } from '../context/teamContext'; import { useTeamContext } from '../context/teamContext';
// Hooks
import { useGame } from '../hook/useGame';
// Util // Util
import { GameState } from '../util/gameState'; import { GameState } from '../util/gameState';
import { ZoneTypes, InitialRegions } from '../util/constants'; import { ZoneTypes, InitialRegions } from '../util/constants';
export const CustomMap = () => { export const CustomMap = () => {
const {zoneType, zoneExtremities, location, gameState} = useTeamContext(); const {teamInfos, zoneType, zoneExtremities, location, gameState} = useTeamContext();
const {enemyLocation, startingArea, lastSentLocation, hasHandicap} = useGame(); const {enemyLocation, startingArea, lastSentLocation, hasHandicap} = teamInfos;
const [centerMap, setCenterMap] = useState(true); const [centerMap, setCenterMap] = useState(true);
const mapRef = useRef(null); const mapRef = useRef(null);

View File

@@ -1,18 +1,16 @@
// React
import { createContext, useContext, useMemo } from "react"; import { createContext, useContext, useMemo } from "react";
// IO
import { io } from "socket.io-client"; import { io } from "socket.io-client";
// Util
import { SOCKET_URL } from "../util/constants";
const IP = "172.16.1.180"; const SocketContext = createContext();
const SOCKET_URL = `ws://${IP}/player`;
const SERVER_URL = `http://${IP}/back`;
export const teamSocket = io(SOCKET_URL, { const teamSocket = io(SOCKET_URL, {path: "/back/socket.io"});
path: "/back/socket.io",
});
export const SocketContext = createContext();
export const SocketProvider = ({ children }) => { export const SocketProvider = ({ children }) => {
const value = useMemo(() => ({ teamSocket, SERVER_URL }), []); const value = useMemo(() => ({ teamSocket }), []);
return ( return (
<SocketContext.Provider value={value}>{children}</SocketContext.Provider> <SocketContext.Provider value={value}>{children}</SocketContext.Provider>

View File

@@ -1,22 +1,22 @@
// React
import { createContext, useContext, useMemo } from "react"; import { createContext, useContext, useMemo } from "react";
import { useSocket } from "./socketContext"; // Hook
import { useSocketAuth } from "../hook/useSocketAuth"; import { useSocketAuth } from "../hook/useSocketAuth";
const teamConnexionContext = createContext(); const TeamConnexionContext = createContext();
export const TeamConnexionProvider = ({ children }) => { export const TeamConnexionProvider = ({ children }) => {
const { teamSocket } = useSocket(); const { login, password: teamId, loggedIn, logout } = useSocketAuth();
const { login, password: teamId, loggedIn, loading, logout } = useSocketAuth(teamSocket, "team_password");
const value = useMemo(() => ({ teamId, login, logout, loggedIn, loading}), [teamId, login, logout, loggedIn, loading]); const value = useMemo(() => ({ teamId, login, logout, loggedIn}), [teamId, login, logout, loggedIn]);
return ( return (
<teamConnexionContext.Provider value={value}> <TeamConnexionContext.Provider value={value}>
{children} {children}
</teamConnexionContext.Provider> </TeamConnexionContext.Provider>
); );
}; };
export const useTeamConnexion = () => { export const useTeamConnexion = () => {
return useContext(teamConnexionContext); return useContext(TeamConnexionContext);
}; };

View File

@@ -1,12 +1,16 @@
// React
import { createContext, useContext, useMemo, useState } from "react"; import { createContext, useContext, useMemo, useState } from "react";
// Context
import { useSocket } from "./socketContext"; import { useSocket } from "./socketContext";
import { useTeamConnexion } from "./teamConnexionContext"; import { useTeamConnexion } from "./teamConnexionContext";
import { GameState } from "../util/gameState"; // Hook
import { useSendDeviceInfo } from "../hook/useSendDeviceInfo"; import { useSendDeviceInfo } from "../hook/useSendDeviceInfo";
import { useLocation } from "../hook/useLocation"; import { useLocation } from "../hook/useLocation";
import { useSocketListener } from "../hook/useSocketListener"; import { useSocketListener } from "../hook/useSocketListener";
// Util
import { GameState } from "../util/gameState";
const teamContext = createContext(); const TeamContext = createContext();
export const TeamProvider = ({children}) => { export const TeamProvider = ({children}) => {
const {teamSocket} = useSocket(); const {teamSocket} = useSocket();
@@ -51,18 +55,17 @@ export const TeamProvider = ({children}) => {
useSocketListener(teamSocket, "logout", logout); useSocketListener(teamSocket, "logout", logout);
const value = useMemo(() => ( const value = useMemo(() => (
{teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location, getLocationAuthorization, startLocationTracking, stopLocationTracking} {teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location, getLocationAuthorization, startLocationTracking, stopLocationTracking}
), [teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location, getLocationAuthorization, startLocationTracking, stopLocationTracking]); ), [teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location, getLocationAuthorization, startLocationTracking, stopLocationTracking]);
return ( return (
<teamContext.Provider value={value}> <TeamContext.Provider value={value}>
{children} {children}
</teamContext.Provider> </TeamContext.Provider>
); );
}; };
export const useTeamContext = () => { export const useTeamContext = () => {
return useContext(teamContext); return useContext(TeamContext);
}; };

View File

@@ -1,33 +1,28 @@
import { useSocket } from "../context/socketContext"; // React
import { useTeamConnexion } from "../context/teamConnexionContext"; import { useCallback } from "react";
import { useTeamContext } from "../context/teamContext"; // Hook
import { useSocketCommands } from "./useSocketCommands";
export const useGame = () => { export const useGame = () => {
const { teamSocket } = useSocket(); const { emitSendPosition, emitCapture } = useSocketCommands();
const { teamId } = useTeamConnexion();
const { teamInfos } = useTeamContext();
function sendCurrentPosition() { const sendCurrentPosition = useCallback(() => {
console.log("Reveal position."); emitSendPosition();
teamSocket.emit("send_position"); }, [emitSendPosition]);
}
function capture(captureCode) {
console.log("Try to capture :", captureCode);
const capture = useCallback((captureCode) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
console.warn("Server did not respond to capture emit."); console.warn("Server timeout: capture", captureCode);
reject(); reject(new Error("Timeout"));
}, 3000); }, 3000);
teamSocket.emit("capture", captureCode, (response) => { emitCapture(captureCode, (response) => {
clearTimeout(timeout); clearTimeout(timeout);
console.log(response.message);
resolve(response); resolve(response);
}); });
}); });
} }, [emitCapture]);
return {...teamInfos, sendCurrentPosition, capture, teamId}; return { sendCurrentPosition, capture };
}; };

View File

@@ -0,0 +1,45 @@
// Rect
import { useCallback, useMemo } from "react";
// Contexts
import { useTeamConnexion } from "../context/teamConnexionContext";
import { useTeamContext } from '../context/teamContext';
// Util
import { SERVER_URL } from "../util/constants";
export const useImageApi = () => {
const { teamId } = useTeamConnexion();
const { teamInfos } = useTeamContext();
const { enemyName } = teamInfos;
const uploadTeamImage = useCallback(async (imageUri) => {
if (!imageUri || !teamId) return;
const data = new FormData();
data.append('file', {
uri: imageUri,
name: 'photo.jpg',
type: 'image/jpeg',
});
try {
const response = await fetch(`${SERVER_URL}/upload?team=${teamId}`, {
method: 'POST',
body: data,
});
if (!response.ok) throw new Error("Échec de l'upload");
return await response.blob();
} catch (error) {
console.error("Erreur uploadImage :", error);
throw error;
}
}, [teamId]);
const enemyImage = useMemo(() => {
if (!teamId || !enemyName) return require('../assets/images/missing_image.jpg');
return {uri: `${SERVER_URL}/photo/enemy?team=${teamId}`};
}, [teamId, enemyName]);
return { enemyImage, uploadTeamImage };
};

View File

@@ -1,34 +1,43 @@
// React
import { useEffect, useState, useCallback } from "react";
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { useEffect, useState } from "react";
export const useLocalStorage = (key, initialValue) => { export const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(initialValue); const [storedValue, setStoredValue] = useState(initialValue);
const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
async function fetchData() { let isMounted = true;
const fetchData = async () => {
try { try {
const item = await AsyncStorage.getItem(key); const item = await AsyncStorage.getItem(key);
setStoredValue(item ? JSON.parse(item) : initialValue); if (isMounted && item !== null) {
setStoredValue(JSON.parse(item));
}
} catch (error) { } catch (error) {
console.log(error); console.error(`Error loading key "${key}":`, error);
}
setLoading(false);
} }
};
fetchData(); fetchData();
}, [initialValue, key]); return () => { isMounted = false; };
}, [key]);
const setValue = async value => { const setValue = useCallback(async (value) => {
try { try {
setLoading(true); setStoredValue((prevValue) => {
const valueToStore = value instanceof Function ? value(storedValue) : value; const valueToStore = value instanceof Function ? value(prevValue) : value;
setStoredValue(valueToStore);
await AsyncStorage.setItem(key, JSON.stringify(valueToStore));
setLoading(false);
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue, loading]; AsyncStorage.setItem(key, JSON.stringify(valueToStore)).catch(err =>
console.error(`Error saving key "${key}":`, err)
);
return valueToStore;
});
} catch (error) {
console.error(error);
}
}, [key]);
return [storedValue, setValue];
}; };

View File

@@ -1,14 +1,17 @@
import { useEffect, useState } from "react"; // React
import { useEffect, useState, useCallback, useMemo } from "react";
import { Alert } from "react-native"; import { Alert } from "react-native";
// Expo
import { defineTask, isTaskRegisteredAsync } from "expo-task-manager"; import { defineTask, isTaskRegisteredAsync } from "expo-task-manager";
import * as Location from 'expo-location'; import * as Location from 'expo-location';
import { useSocket } from "../context/socketContext"; // Hook
import { useSocketCommands } from "./useSocketCommands";
export const useLocation = (timeInterval, distanceInterval) => { export const useLocation = (timeInterval, distanceInterval) => {
const { emitUpdatePosition } = useSocketCommands();
const [location, setLocation] = useState(null); // [latitude, longitude] const [location, setLocation] = useState(null); // [latitude, longitude]
const { teamSocket } = useSocket();
const LOCATION_TASK_NAME = "background-location-task"; const LOCATION_TASK_NAME = "background-location-task";
const locationUpdateParameters = { const locationUpdateParameters = useMemo(() => ({
accuracy: Location.Accuracy.High, accuracy: Location.Accuracy.High,
distanceInterval: distanceInterval, // Update every 10 meters distanceInterval: distanceInterval, // Update every 10 meters
timeInterval: timeInterval, // Minimum interval in ms timeInterval: timeInterval, // Minimum interval in ms
@@ -19,7 +22,7 @@ export const useLocation = (timeInterval, distanceInterval) => {
notificationBody: "L'application utilise votre position en arrière plan.", notificationBody: "L'application utilise votre position en arrière plan.",
notificationColor: "#FF0000", // (Android) Notification icon color notificationColor: "#FF0000", // (Android) Notification icon color
}, },
}; }), [distanceInterval, timeInterval]);
defineTask(LOCATION_TASK_NAME, async ({ data, error }) => { defineTask(LOCATION_TASK_NAME, async ({ data, error }) => {
if (error) { if (error) {
@@ -37,19 +40,14 @@ export const useLocation = (timeInterval, distanceInterval) => {
} catch (e) { } catch (e) {
console.warn("setLocation failed (probably in background):", e); console.warn("setLocation failed (probably in background):", e);
} }
console.log("Sending position :", new_location); emitUpdatePosition(new_location);
teamSocket.emit("update_position", new_location);
} else { } else {
console.log("No location measured."); console.log("No location measured.");
} }
} }
}); });
useEffect(() => { const getLocationAuthorization = useCallback(async () => {
getLocationAuthorization();
}, []);
async function getLocationAuthorization() {
const { status : statusForeground } = await Location.requestForegroundPermissionsAsync(); const { status : statusForeground } = await Location.requestForegroundPermissionsAsync();
const { status : statusBackground } = await Location.requestBackgroundPermissionsAsync(); const { status : statusBackground } = await Location.requestBackgroundPermissionsAsync();
if (statusForeground !== "granted" || statusBackground !== "granted") { if (statusForeground !== "granted" || statusBackground !== "granted") {
@@ -58,23 +56,27 @@ export const useLocation = (timeInterval, distanceInterval) => {
} else { } else {
return true; return true;
} }
} }, []);
async function startLocationTracking() { const startLocationTracking = useCallback(async () => {
if (await getLocationAuthorization()) { if (await getLocationAuthorization()) {
if (!(await isTaskRegisteredAsync(LOCATION_TASK_NAME))) { if (!(await isTaskRegisteredAsync(LOCATION_TASK_NAME))) {
await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, locationUpdateParameters); await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, locationUpdateParameters);
console.log("Location tracking started."); console.log("Location tracking started.");
} }
} }
} }, [getLocationAuthorization, locationUpdateParameters]);
async function stopLocationTracking() { const stopLocationTracking = useCallback(async () => {
if (await isTaskRegisteredAsync(LOCATION_TASK_NAME)) { if (await isTaskRegisteredAsync(LOCATION_TASK_NAME)) {
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME); await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
console.log("Location tracking stopped."); console.log("Location tracking stopped.");
} }
} }, []);
useEffect(() => {
getLocationAuthorization();
}, [getLocationAuthorization]);
return [location, getLocationAuthorization, startLocationTracking, stopLocationTracking]; return [location, getLocationAuthorization, startLocationTracking, stopLocationTracking];
}; };

View File

@@ -1,11 +1,13 @@
import { useState, } from 'react'; // React
import { useState, useCallback } from 'react';
import { Alert } from 'react-native'; import { Alert } from 'react-native';
// Expo
import { launchImageLibraryAsync, requestMediaLibraryPermissionsAsync } from 'expo-image-picker'; import { launchImageLibraryAsync, requestMediaLibraryPermissionsAsync } from 'expo-image-picker';
export const usePickImage = () => { export const usePickImage = () => {
const [image, setImage] = useState(null); const [image, setImage] = useState(null);
const pickImage = async () => { const pickImage = useCallback(async () => {
try { try {
const permissionResult = await requestMediaLibraryPermissionsAsync(); const permissionResult = await requestMediaLibraryPermissionsAsync();
@@ -13,7 +15,6 @@ export const usePickImage = () => {
Alert.alert("Permission refusée", "Activez l'accès au stockage ou à la gallerie dans les paramètres."); Alert.alert("Permission refusée", "Activez l'accès au stockage ou à la gallerie dans les paramètres.");
return; return;
} }
let result = await launchImageLibraryAsync({ let result = await launchImageLibraryAsync({
mediaTypes: ['images'], mediaTypes: ['images'],
allowsMultipleSelection: false, allowsMultipleSelection: false,
@@ -28,31 +29,11 @@ export const usePickImage = () => {
else { else {
console.log('Image picker cancelled.'); console.log('Image picker cancelled.');
} }
} } catch (error) {
catch (error) {
console.error('Error picking image;', error); console.error('Error picking image;', error);
Alert.alert('Erreur', "Une erreur est survenue lors de la sélection d'une image."); Alert.alert('Erreur', "Une erreur est survenue lors de la sélection d'une image.");
} }
}; }, []);
function sendImage(location) { return {image, pickImage};
if (image) {
let data = new FormData();
data.append('file', {
uri: image.uri,
name: 'photo.jpg',
type: 'image/jpeg',
});
fetch(location , {
method: 'POST',
body: data,
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
}
return {image, pickImage, sendImage};
}; };

View File

@@ -1,35 +1,47 @@
import { useEffect } from 'react'; // React
import { useEffect, useRef } from 'react';
import DeviceInfo from 'react-native-device-info'; import DeviceInfo from 'react-native-device-info';
import { useSocket } from "../context/socketContext"; // Context
import { useTeamConnexion } from "../context/teamConnexionContext"; import { useTeamConnexion } from "../context/teamConnexionContext";
// Hook
import { useSocketCommands } from "./useSocketCommands";
export const useSendDeviceInfo = () => { export const useSendDeviceInfo = () => {
const batteryUpdateTimeout = 5*60*1000; const { emitBattery, emitDeviceInfo } = useSocketCommands();
const { teamSocket } = useSocket();
const { loggedIn } = useTeamConnexion(); const { loggedIn } = useTeamConnexion();
const isMounted = useRef(true);
useEffect(() => { useEffect(() => {
isMounted.current = true;
if (!loggedIn) return; if (!loggedIn) return;
const sendInfo = async () => { const sendInfo = async () => {
const brand = DeviceInfo.getBrand(); const [brand, model, name] = await Promise.all([
const model = DeviceInfo.getModel(); DeviceInfo.getBrand(),
const name = await DeviceInfo.getDeviceName(); DeviceInfo.getModel(),
teamSocket.emit('device_info', {model: brand + " " + model, name: name}); DeviceInfo.getDeviceName()
]);
if (!isMounted) return;
emitDeviceInfo({model: brand + " " + model, name: name});
}; };
const sendBattery = async () => { const sendBattery = async () => {
const level = await DeviceInfo.getBatteryLevel(); const level = await DeviceInfo.getBatteryLevel();
teamSocket.emit('battery_update', Math.round(level * 100)); if (!isMounted) return;
emitBattery(Math.round(level * 100));
}; };
sendInfo(); sendInfo();
sendBattery(); sendBattery();
const batteryCheckInterval = setInterval(() => sendBattery(), batteryUpdateTimeout); const batteryCheckInterval = setInterval(() => sendBattery(), 5*60*1000); // 5 minutes
return () => clearInterval(batteryCheckInterval); return () => {
}, [batteryUpdateTimeout, loggedIn, teamSocket]); isMounted.current = false;
clearInterval(batteryCheckInterval);
};
}, [emitBattery, emitDeviceInfo, loggedIn]);
return null; return null;
}; };

View File

@@ -1,72 +1,60 @@
import { useEffect, useState } from 'react'; // React
import { useState, useEffect, useCallback, useRef } from 'react';
// Hook
import { useLocalStorage } from './useLocalStorage'; import { useLocalStorage } from './useLocalStorage';
import { useSocketCommands } from "./useSocketCommands";
const LOGIN_MESSAGE = "login"; export const useSocketAuth = () => {
const LOGOUT_MESSAGE = "logout"; const { emitLogin, emitLogout } = useSocketCommands();
export const useSocketAuth = (socket, passwordName) => {
const [loggedIn, setLoggedIn] = useState(false); const [loggedIn, setLoggedIn] = useState(false);
const [loading, setLoading] = useState(true); const [savedPassword, setSavedPassword] = useLocalStorage("team_password", null);
const [waitingForResponse, setWaitingForResponse] = useState(false); const isMounted = useRef(true);
const [hasTriedSavedPassword, setHasTriedSavedPassword] = useState(false);
const [savedPassword, setSavedPassword, savedPasswordLoading] = useLocalStorage(passwordName, null);
useEffect(() => { useEffect(() => {
if (!loading && !hasTriedSavedPassword) { isMounted.current = true;
console.log("Try to log in with saved password :", savedPassword); return () => {
setWaitingForResponse(true); isMounted.current = false;
};
const timeout = setTimeout(() => { }, []);
console.warn("Server did not respond to login emit.");
setWaitingForResponse(false);
}, 3000);
socket.emit(LOGIN_MESSAGE, savedPassword, (response) => {
clearTimeout(timeout);
console.log(response.message);
setLoggedIn(response.isLoggedIn);
setWaitingForResponse(false);
});
setHasTriedSavedPassword(true);
}
}, [hasTriedSavedPassword, loading, savedPassword, socket]);
function login(password) {
console.log("Try to log in with :", password);
setSavedPassword(password);
setWaitingForResponse(true);
const login = useCallback((password) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
if (!isMounted.current) return;
console.warn("Server did not respond to login emit."); console.warn("Server did not respond to login emit.");
setWaitingForResponse(false);
reject(); reject();
}, 3000); }, 2000);
socket.emit(LOGIN_MESSAGE, password, (response) => { emitLogin(password, (response) => {
clearTimeout(timeout); clearTimeout(timeout);
console.log(response.message);
setLoggedIn(response.isLoggedIn);
setWaitingForResponse(false);
resolve(response);
});
});
}
function logout() { if (!isMounted.current) return;
console.log("Logout");
setSavedPassword(null); if (response.isLoggedIn) {
console.log("Log in accepted.");
setLoggedIn(true);
setSavedPassword(password);
resolve(response);
} else {
console.log("Log in refused.");
setLoggedIn(false); setLoggedIn(false);
socket.emit(LOGOUT_MESSAGE); reject();
} }
});
});
}, [emitLogin, setSavedPassword]);
useEffect(() => { useEffect(() => {
if(!waitingForResponse && !savedPasswordLoading) { if (!loggedIn && savedPassword) {
setLoading(false); login(savedPassword);
} else {
setLoading(true);
} }
}, [waitingForResponse, savedPasswordLoading]); }, [loggedIn, login, savedPassword]);
return {login, logout, password: savedPassword, loggedIn, loading}; const logout = useCallback(() => {
setLoggedIn(false);
setSavedPassword(null);
emitLogout();
}, [emitLogout, setSavedPassword]);
return {login, logout, password: savedPassword, loggedIn};
}; };

View File

@@ -0,0 +1,52 @@
// React
import { useCallback } from 'react';
// Context
import { useSocket } from "../context/socketContext";
export const useSocketCommands = () => {
const { teamSocket } = useSocket();
const emitLogin = useCallback((password, callback) => {
if (!teamSocket?.connected) return;
console.log("Try to log in with :", password);
teamSocket.emit("login", password, callback);
}, [teamSocket]);
const emitLogout = useCallback(() => {
if (!teamSocket?.connected) return;
console.log("Logout.");
teamSocket.emit("logout");
}, [teamSocket]);
const emitSendPosition = useCallback(() => {
if (!teamSocket?.connected) return;
console.log("Reveal position.");
teamSocket.emit("send_position");
}, [teamSocket]);
const emitUpdatePosition = useCallback((location) => {
if (!teamSocket?.connected) return;
console.log("Update position :", location);
teamSocket.emit("update_position", location);
}, [teamSocket]);
const emitCapture = useCallback((captureCode, callback) => {
if (!teamSocket?.connected) return;
console.log("Try to capture :", captureCode);
teamSocket.emit("capture", captureCode, callback);
}, [teamSocket]);
const emitBattery = useCallback((level) => {
if (!teamSocket?.connected) return;
console.log("Send battery level.");
teamSocket.emit('battery_update', level);
}, [teamSocket]);
const emitDeviceInfo = useCallback((infos) => {
if (!teamSocket?.connected) return;
console.log("Send device infos.");
teamSocket.emit('device_info', infos);
}, [teamSocket]);
return { emitLogin, emitLogout, emitSendPosition, emitUpdatePosition, emitCapture, emitBattery, emitDeviceInfo };
};

View File

@@ -1,3 +1,4 @@
// React
import { useEffect } from "react"; import { useEffect } from "react";
export const useSocketListener = (socket, event, callback) => { export const useSocketListener = (socket, event, callback) => {

View File

@@ -1,3 +1,4 @@
// React
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const useTimeDifference = (refTime, timeout) => { export const useTimeDifference = (refTime, timeout) => {

View File

@@ -1,3 +1,6 @@
export const SERVER_URL = process.env.EXPO_PUBLIC_SERVER_URL;
export const SOCKET_URL = process.env.EXPO_PUBLIC_SOCKET_URL;
export const InitialRegions = { export const InitialRegions = {
paris : { paris : {
latitude: 48.864, latitude: 48.864,

View File

@@ -56,7 +56,7 @@ export default {
getNewTeamId() { getNewTeamId() {
let id = randint(1_000_000); let id = randint(1_000_000);
while (this.teams.find(t => t.id === id)) id = randint(1_000_000); while (this.teams.find(t => t.id === id)) id = randint(1_000_000);
return id; return id.toString().padStart(6, '0');
}, },
checkEndGame() { checkEndGame() {

View File

@@ -31,7 +31,7 @@ const upload = multer({
fileFilter: function (req, file, callback) { fileFilter: function (req, file, callback) {
if (ALLOWED_MIME.indexOf(file.mimetype) == -1) { if (ALLOWED_MIME.indexOf(file.mimetype) == -1) {
callback(null, false); callback(null, false);
} else if (!game.getTeam(Number(req.query.team))) { } else if (!game.getTeam(req.query.team)) {
callback(null, false); callback(null, false);
} else { } else {
callback(null, true); callback(null, true);
@@ -58,9 +58,9 @@ export function initPhotoUpload() {
}) })
//App handler for serving the photo of a team given its secret ID //App handler for serving the photo of a team given its secret ID
app.get("/photo/my", (req, res) => { app.get("/photo/my", (req, res) => {
let team = game.getTeam(Number(req.query.team)); let team = game.getTeam(req.query.team);
if (team) { if (team) {
const imagePath = path.join(process.cwd(), UPLOAD_DIR, team.id.toString()); const imagePath = path.join(process.cwd(), UPLOAD_DIR, team.id);
res.set("Content-Type", "image/png") res.set("Content-Type", "image/png")
res.set("Access-Control-Allow-Origin", "*"); res.set("Access-Control-Allow-Origin", "*");
res.sendFile(fs.existsSync(imagePath) ? imagePath : path.join(process.cwd(), "images", "missing_image.jpg")); res.sendFile(fs.existsSync(imagePath) ? imagePath : path.join(process.cwd(), "images", "missing_image.jpg"));
@@ -70,9 +70,9 @@ export function initPhotoUpload() {
}) })
//App handler for serving the photo of the team chased by the team given by its secret ID //App handler for serving the photo of the team chased by the team given by its secret ID
app.get("/photo/enemy", (req, res) => { app.get("/photo/enemy", (req, res) => {
let team = game.getTeam(Number(req.query.team)); let team = game.getTeam(req.query.team);
if (team) { if (team) {
const imagePath = path.join(process.cwd(), UPLOAD_DIR, team.chasing.toString()); const imagePath = path.join(process.cwd(), UPLOAD_DIR, team.chasing);
res.set("Content-Type", "image/png") res.set("Content-Type", "image/png")
res.set("Access-Control-Allow-Origin", "*"); res.set("Access-Control-Allow-Origin", "*");
res.sendFile(fs.existsSync(imagePath) ? imagePath : path.join(process.cwd(), "images", "missing_image.jpg")); res.sendFile(fs.existsSync(imagePath) ? imagePath : path.join(process.cwd(), "images", "missing_image.jpg"));