From 05a60612c68ef4c56f82c7c1c200621055f933c8 Mon Sep 17 00:00:00 2001 From: Sebastien Riviere Date: Tue, 17 Feb 2026 14:32:37 +0100 Subject: [PATCH] Fix photos + API hooks + cleaning --- .gitignore | 3 + .../traque_14_12_25/compte_rendu.txt | 3 +- mobile/docs/setup.md | 20 +++- mobile/traque-app/app/index.jsx | 63 +++++++------ mobile/traque-app/app/interface.jsx | 15 ++- mobile/traque-app/components/drawer.jsx | 19 ++-- mobile/traque-app/components/layer.jsx | 1 + mobile/traque-app/components/map.jsx | 6 +- mobile/traque-app/context/socketContext.jsx | 16 ++-- .../context/teamConnexionContext.jsx | 16 ++-- mobile/traque-app/context/teamContext.jsx | 15 +-- mobile/traque-app/hook/useGame.jsx | 33 +++---- mobile/traque-app/hook/useImageApi.jsx | 45 +++++++++ mobile/traque-app/hook/useLocalStorage.jsx | 43 +++++---- mobile/traque-app/hook/useLocation.jsx | 36 +++---- mobile/traque-app/hook/usePickImage.jsx | 33 ++----- mobile/traque-app/hook/useSendDeviceInfo.jsx | 38 +++++--- mobile/traque-app/hook/useSocketAuth.jsx | 94 ++++++++----------- mobile/traque-app/hook/useSocketCommands.jsx | 52 ++++++++++ mobile/traque-app/hook/useSocketListener.jsx | 3 +- mobile/traque-app/hook/useTimeDifference.jsx | 1 + mobile/traque-app/util/constants.js | 5 +- server/traque-back/game.js | 2 +- server/traque-back/photo.js | 10 +- 24 files changed, 341 insertions(+), 231 deletions(-) create mode 100644 mobile/traque-app/hook/useImageApi.jsx create mode 100644 mobile/traque-app/hook/useSocketCommands.jsx diff --git a/.gitignore b/.gitignore index d277a2b..b98cfd3 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,6 @@ yarn.lock keys/ temp.* temp/ +.env +.env.development +.env.production diff --git a/docs/historique/traque_14_12_25/compte_rendu.txt b/docs/historique/traque_14_12_25/compte_rendu.txt index 365976e..084179a 100644 --- a/docs/historique/traque_14_12_25/compte_rendu.txt +++ b/docs/historique/traque_14_12_25/compte_rendu.txt @@ -10,7 +10,8 @@ Problèmes [ ] 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 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 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 diff --git a/mobile/docs/setup.md b/mobile/docs/setup.md index 6796c9c..82cc306 100644 --- a/mobile/docs/setup.md +++ b/mobile/docs/setup.md @@ -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. ## 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 * [APK](#apk) : Create and install ## 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 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). diff --git a/mobile/traque-app/app/index.jsx b/mobile/traque-app/app/index.jsx index 347dd40..4580d4a 100644 --- a/mobile/traque-app/app/index.jsx +++ b/mobile/traque-app/app/index.jsx @@ -8,22 +8,22 @@ import { CustomButton } from '../components/button'; import { CustomImage } from '../components/image'; import { CustomTextInput } from '../components/input'; // Contexts -import { useSocket } from "../context/socketContext"; import { useTeamConnexion } from "../context/teamConnexionContext"; import { useTeamContext } from "../context/teamContext"; // Hooks import { usePickImage } from '../hook/usePickImage'; +import { useImageApi } from '../hook/useImageApi'; // Util import { Colors } from '../util/colors'; const Index = () => { const router = useRouter(); - const {SERVER_URL} = useSocket(); - const {login, loggedIn, loading} = useTeamConnexion(); + const {login, loggedIn} = useTeamConnexion(); const {getLocationAuthorization, stopLocationTracking} = useTeamContext(); - const {image, pickImage, sendImage} = usePickImage(); - const [teamID, setTeamID] = useState(""); + const {image, pickImage} = usePickImage(); + const [teamId, setTeamId] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); + const { uploadTeamImage } = useImageApi(); // Disbaling location tracking useEffect(() => { @@ -32,32 +32,37 @@ const Index = () => { // Routeur useEffect(() => { - if (!loading && loggedIn) { + if (loggedIn) { + uploadTeamImage(image?.uri); router.replace("/interface"); } - }, [router, loggedIn, loading]); + }, [router, loggedIn, uploadTeamImage, image]); - function handleSubmit() { - if (!isSubmitting && !loading) { - setIsSubmitting(true); - if (getLocationAuthorization()) { - login(parseInt(teamID)) - .then((response) => { - if (response.isLoggedIn) { - sendImage(`${SERVER_URL}/upload?team=${teamID}`); - } else { - Alert.alert("Échec", "L'ID d'équipe est inconnu."); - } - setIsSubmitting(false); - }) - .catch(() => { - Alert.alert("Échec", "La connection au serveur a échoué."); - setIsSubmitting(false); - }); - } - setTeamID(""); + const handleSubmit = async () => { + if (isSubmitting || !getLocationAuthorization()) return; + + setIsSubmitting(true); + + const regex = /^\d{6}$/; + 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) { + setTeamId(""); + } else { + 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); + } + }; return ( @@ -67,7 +72,7 @@ const Index = () => { LA TRAQUE - + Appuyer pour changer la photo d'équipe @@ -75,7 +80,7 @@ const Index = () => { - + diff --git a/mobile/traque-app/app/interface.jsx b/mobile/traque-app/app/interface.jsx index 0f4f573..9a831a1 100644 --- a/mobile/traque-app/app/interface.jsx +++ b/mobile/traque-app/app/interface.jsx @@ -20,9 +20,10 @@ import { Colors } from '../util/colors'; const Interface = () => { const router = useRouter(); - const {messages, nextZoneDate, isShrinking, startLocationTracking, stopLocationTracking, gameState} = useTeamContext(); - const {loggedIn, logout, loading} = useTeamConnexion(); - const {name, ready, captured, locationSendDeadline, sendCurrentPosition, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = useGame(); + const {teamInfos, messages, nextZoneDate, isShrinking, startLocationTracking, stopLocationTracking, gameState} = useTeamContext(); + const {name, ready, captured, locationSendDeadline, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = teamInfos; + const {loggedIn, logout} = useTeamConnexion(); + const {sendCurrentPosition} = useGame(); const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000); const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000); const [timeLeftOutOfZone] = useTimeDifference(outOfZoneDeadline, 1000); @@ -48,12 +49,10 @@ const Interface = () => { // Router useEffect(() => { - if (!loading) { - if (!loggedIn) { - router.replace("/"); - } + if (!loggedIn) { + router.replace("/"); } - }, [router, loggedIn, loading]); + }, [router, loggedIn]); // Activating geolocation tracking useEffect(() => { diff --git a/mobile/traque-app/components/drawer.jsx b/mobile/traque-app/components/drawer.jsx index a70f155..a40b1c6 100644 --- a/mobile/traque-app/components/drawer.jsx +++ b/mobile/traque-app/components/drawer.jsx @@ -9,7 +9,6 @@ import { CustomTextInput } from './input'; import { Stat } from './stat'; // Contexts import { useTeamContext } from '../context/teamContext'; -import { useSocket } from '../context/socketContext'; // Hooks import { useTimeDifference } from '../hook/useTimeDifference'; import { useGame } from '../hook/useGame'; @@ -17,17 +16,18 @@ import { useGame } from '../hook/useGame'; import { GameState } from '../util/gameState'; import { Colors } from '../util/colors'; import { secondsToHHMMSS } from '../util/functions'; +import { useImageApi } from '../hook/useImageApi'; export const Drawer = ({ height }) => { const [collapsibleState, setCollapsibleState] = useState(true); const [enemyCaptureCode, setEnemyCaptureCode] = useState(""); - const {SERVER_URL} = useSocket(); - const {gameState, startDate} = useTeamContext(); - const {capture, enemyName, captureCode, name, teamId, distance, finishDate, nCaptures, nSentLocation, hasHandicap} = useGame(); + const {teamInfos, gameState, startDate} = useTeamContext(); + const {enemyName, captureCode, name, distance, finishDate, nCaptures, nSentLocation, hasHandicap} = teamInfos; + const {capture} = useGame(); 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 captureStatusColor = {0: "#777", 1: "#FFA500", 2: "#FF6B6B", 3: "#81C784"}; + const { enemyImage } = useImageApi(); const avgSpeed = useMemo(() => { const hours = (finishDate ? (finishDate - startDate) : timeSinceStart*1000) / 1000 / 3600; @@ -48,11 +48,6 @@ export const Drawer = ({ height }) => { } }, [captureStatus]); - // Refresh the image - useEffect(() => { - setEnemyImageURI(`${SERVER_URL}/photo/enemy?team=${teamId}&t=${new Date().getTime()}`); - }, [SERVER_URL, enemyName, teamId]); - const handleCapture = () => { if (captureStatus != 1) { setCaptureStatus(1); @@ -86,8 +81,8 @@ export const Drawer = ({ height }) => { } { gameState == GameState.PLAYING && !hasHandicap && - {{"Cible (" + (enemyName ?? "Indisponible") + ")"}} - {} + {"Cible (" + (enemyName ?? "Indisponible") + ")"} + diff --git a/mobile/traque-app/components/layer.jsx b/mobile/traque-app/components/layer.jsx index 8790cd1..de659fe 100644 --- a/mobile/traque-app/components/layer.jsx +++ b/mobile/traque-app/components/layer.jsx @@ -1,3 +1,4 @@ +// React import { Fragment } from 'react'; import { Polygon } from 'react-native-maps'; import { circleToPolygon } from '../util/functions'; diff --git a/mobile/traque-app/components/map.jsx b/mobile/traque-app/components/map.jsx index 06ffe89..8375f25 100644 --- a/mobile/traque-app/components/map.jsx +++ b/mobile/traque-app/components/map.jsx @@ -7,15 +7,13 @@ import LinearGradient from 'react-native-linear-gradient'; import { DashedCircle, InvertedCircle, InvertedPolygon } from './layer'; // Contexts import { useTeamContext } from '../context/teamContext'; -// Hooks -import { useGame } from '../hook/useGame'; // Util import { GameState } from '../util/gameState'; import { ZoneTypes, InitialRegions } from '../util/constants'; export const CustomMap = () => { - const {zoneType, zoneExtremities, location, gameState} = useTeamContext(); - const {enemyLocation, startingArea, lastSentLocation, hasHandicap} = useGame(); + const {teamInfos, zoneType, zoneExtremities, location, gameState} = useTeamContext(); + const {enemyLocation, startingArea, lastSentLocation, hasHandicap} = teamInfos; const [centerMap, setCenterMap] = useState(true); const mapRef = useRef(null); diff --git a/mobile/traque-app/context/socketContext.jsx b/mobile/traque-app/context/socketContext.jsx index 59d9329..bb84505 100644 --- a/mobile/traque-app/context/socketContext.jsx +++ b/mobile/traque-app/context/socketContext.jsx @@ -1,18 +1,16 @@ +// React import { createContext, useContext, useMemo } from "react"; +// IO import { io } from "socket.io-client"; +// Util +import { SOCKET_URL } from "../util/constants"; -const IP = "172.16.1.180"; -const SOCKET_URL = `ws://${IP}/player`; -const SERVER_URL = `http://${IP}/back`; +const SocketContext = createContext(); -export const teamSocket = io(SOCKET_URL, { - path: "/back/socket.io", -}); - -export const SocketContext = createContext(); +const teamSocket = io(SOCKET_URL, {path: "/back/socket.io"}); export const SocketProvider = ({ children }) => { - const value = useMemo(() => ({ teamSocket, SERVER_URL }), []); + const value = useMemo(() => ({ teamSocket }), []); return ( {children} diff --git a/mobile/traque-app/context/teamConnexionContext.jsx b/mobile/traque-app/context/teamConnexionContext.jsx index 3b63297..abc84eb 100644 --- a/mobile/traque-app/context/teamConnexionContext.jsx +++ b/mobile/traque-app/context/teamConnexionContext.jsx @@ -1,22 +1,22 @@ +// React import { createContext, useContext, useMemo } from "react"; -import { useSocket } from "./socketContext"; +// Hook import { useSocketAuth } from "../hook/useSocketAuth"; -const teamConnexionContext = createContext(); +const TeamConnexionContext = createContext(); export const TeamConnexionProvider = ({ children }) => { - const { teamSocket } = useSocket(); - const { login, password: teamId, loggedIn, loading, logout } = useSocketAuth(teamSocket, "team_password"); + const { login, password: teamId, loggedIn, logout } = useSocketAuth(); - 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 ( - + {children} - + ); }; export const useTeamConnexion = () => { - return useContext(teamConnexionContext); + return useContext(TeamConnexionContext); }; diff --git a/mobile/traque-app/context/teamContext.jsx b/mobile/traque-app/context/teamContext.jsx index 3f4388e..65c3ed3 100644 --- a/mobile/traque-app/context/teamContext.jsx +++ b/mobile/traque-app/context/teamContext.jsx @@ -1,12 +1,16 @@ +// React import { createContext, useContext, useMemo, useState } from "react"; +// Context import { useSocket } from "./socketContext"; import { useTeamConnexion } from "./teamConnexionContext"; -import { GameState } from "../util/gameState"; +// Hook import { useSendDeviceInfo } from "../hook/useSendDeviceInfo"; import { useLocation } from "../hook/useLocation"; import { useSocketListener } from "../hook/useSocketListener"; +// Util +import { GameState } from "../util/gameState"; -const teamContext = createContext(); +const TeamContext = createContext(); export const TeamProvider = ({children}) => { const {teamSocket} = useSocket(); @@ -51,18 +55,17 @@ export const TeamProvider = ({children}) => { useSocketListener(teamSocket, "logout", logout); - 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]); return ( - + {children} - + ); }; export const useTeamContext = () => { - return useContext(teamContext); + return useContext(TeamContext); }; diff --git a/mobile/traque-app/hook/useGame.jsx b/mobile/traque-app/hook/useGame.jsx index a02bdc1..ba13cba 100644 --- a/mobile/traque-app/hook/useGame.jsx +++ b/mobile/traque-app/hook/useGame.jsx @@ -1,33 +1,28 @@ -import { useSocket } from "../context/socketContext"; -import { useTeamConnexion } from "../context/teamConnexionContext"; -import { useTeamContext } from "../context/teamContext"; +// React +import { useCallback } from "react"; +// Hook +import { useSocketCommands } from "./useSocketCommands"; export const useGame = () => { - const { teamSocket } = useSocket(); - const { teamId } = useTeamConnexion(); - const { teamInfos } = useTeamContext(); + const { emitSendPosition, emitCapture } = useSocketCommands(); - function sendCurrentPosition() { - console.log("Reveal position."); - teamSocket.emit("send_position"); - } + const sendCurrentPosition = useCallback(() => { + emitSendPosition(); + }, [emitSendPosition]); - function capture(captureCode) { - console.log("Try to capture :", captureCode); - + const capture = useCallback((captureCode) => { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { - console.warn("Server did not respond to capture emit."); - reject(); + console.warn("Server timeout: capture", captureCode); + reject(new Error("Timeout")); }, 3000); - teamSocket.emit("capture", captureCode, (response) => { + emitCapture(captureCode, (response) => { clearTimeout(timeout); - console.log(response.message); resolve(response); }); }); - } + }, [emitCapture]); - return {...teamInfos, sendCurrentPosition, capture, teamId}; + return { sendCurrentPosition, capture }; }; diff --git a/mobile/traque-app/hook/useImageApi.jsx b/mobile/traque-app/hook/useImageApi.jsx new file mode 100644 index 0000000..7149c45 --- /dev/null +++ b/mobile/traque-app/hook/useImageApi.jsx @@ -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 }; +}; diff --git a/mobile/traque-app/hook/useLocalStorage.jsx b/mobile/traque-app/hook/useLocalStorage.jsx index 946aca8..64bb163 100644 --- a/mobile/traque-app/hook/useLocalStorage.jsx +++ b/mobile/traque-app/hook/useLocalStorage.jsx @@ -1,34 +1,43 @@ +// React +import { useEffect, useState, useCallback } from "react"; import AsyncStorage from '@react-native-async-storage/async-storage'; -import { useEffect, useState } from "react"; export const useLocalStorage = (key, initialValue) => { const [storedValue, setStoredValue] = useState(initialValue); - const [loading, setLoading] = useState(true); useEffect(() => { - async function fetchData() { + let isMounted = true; + + const fetchData = async () => { try { const item = await AsyncStorage.getItem(key); - setStoredValue(item ? JSON.parse(item) : initialValue); + if (isMounted && item !== null) { + setStoredValue(JSON.parse(item)); + } } catch (error) { - console.log(error); + console.error(`Error loading key "${key}":`, error); } - setLoading(false); - } + }; + fetchData(); - }, [initialValue, key]); + return () => { isMounted = false; }; + }, [key]); - const setValue = async value => { + const setValue = useCallback(async (value) => { try { - setLoading(true); - const valueToStore = value instanceof Function ? value(storedValue) : value; - setStoredValue(valueToStore); - await AsyncStorage.setItem(key, JSON.stringify(valueToStore)); - setLoading(false); + setStoredValue((prevValue) => { + const valueToStore = value instanceof Function ? value(prevValue) : value; + + AsyncStorage.setItem(key, JSON.stringify(valueToStore)).catch(err => + console.error(`Error saving key "${key}":`, err) + ); + + return valueToStore; + }); } catch (error) { - console.log(error); + console.error(error); } - }; + }, [key]); - return [storedValue, setValue, loading]; + return [storedValue, setValue]; }; diff --git a/mobile/traque-app/hook/useLocation.jsx b/mobile/traque-app/hook/useLocation.jsx index e8936d5..b616984 100644 --- a/mobile/traque-app/hook/useLocation.jsx +++ b/mobile/traque-app/hook/useLocation.jsx @@ -1,14 +1,17 @@ -import { useEffect, useState } from "react"; +// React +import { useEffect, useState, useCallback, useMemo } from "react"; import { Alert } from "react-native"; +// Expo import { defineTask, isTaskRegisteredAsync } from "expo-task-manager"; import * as Location from 'expo-location'; -import { useSocket } from "../context/socketContext"; +// Hook +import { useSocketCommands } from "./useSocketCommands"; export const useLocation = (timeInterval, distanceInterval) => { + const { emitUpdatePosition } = useSocketCommands(); const [location, setLocation] = useState(null); // [latitude, longitude] - const { teamSocket } = useSocket(); const LOCATION_TASK_NAME = "background-location-task"; - const locationUpdateParameters = { + const locationUpdateParameters = useMemo(() => ({ accuracy: Location.Accuracy.High, distanceInterval: distanceInterval, // Update every 10 meters 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.", notificationColor: "#FF0000", // (Android) Notification icon color }, - }; + }), [distanceInterval, timeInterval]); defineTask(LOCATION_TASK_NAME, async ({ data, error }) => { if (error) { @@ -37,19 +40,14 @@ export const useLocation = (timeInterval, distanceInterval) => { } catch (e) { console.warn("setLocation failed (probably in background):", e); } - console.log("Sending position :", new_location); - teamSocket.emit("update_position", new_location); + emitUpdatePosition(new_location); } else { console.log("No location measured."); } } }); - useEffect(() => { - getLocationAuthorization(); - }, []); - - async function getLocationAuthorization() { + const getLocationAuthorization = useCallback(async () => { const { status : statusForeground } = await Location.requestForegroundPermissionsAsync(); const { status : statusBackground } = await Location.requestBackgroundPermissionsAsync(); if (statusForeground !== "granted" || statusBackground !== "granted") { @@ -58,23 +56,27 @@ export const useLocation = (timeInterval, distanceInterval) => { } else { return true; } - } + }, []); - async function startLocationTracking() { + const startLocationTracking = useCallback(async () => { if (await getLocationAuthorization()) { if (!(await isTaskRegisteredAsync(LOCATION_TASK_NAME))) { await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, locationUpdateParameters); console.log("Location tracking started."); } } - } + }, [getLocationAuthorization, locationUpdateParameters]); - async function stopLocationTracking() { + const stopLocationTracking = useCallback(async () => { if (await isTaskRegisteredAsync(LOCATION_TASK_NAME)) { await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME); console.log("Location tracking stopped."); } - } + }, []); + + useEffect(() => { + getLocationAuthorization(); + }, [getLocationAuthorization]); return [location, getLocationAuthorization, startLocationTracking, stopLocationTracking]; }; diff --git a/mobile/traque-app/hook/usePickImage.jsx b/mobile/traque-app/hook/usePickImage.jsx index 778365b..4135f3d 100644 --- a/mobile/traque-app/hook/usePickImage.jsx +++ b/mobile/traque-app/hook/usePickImage.jsx @@ -1,11 +1,13 @@ -import { useState, } from 'react'; +// React +import { useState, useCallback } from 'react'; import { Alert } from 'react-native'; +// Expo import { launchImageLibraryAsync, requestMediaLibraryPermissionsAsync } from 'expo-image-picker'; export const usePickImage = () => { const [image, setImage] = useState(null); - const pickImage = async () => { + const pickImage = useCallback(async () => { try { 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."); return; } - let result = await launchImageLibraryAsync({ mediaTypes: ['images'], allowsMultipleSelection: false, @@ -28,31 +29,11 @@ export const usePickImage = () => { else { console.log('Image picker cancelled.'); } - } - catch (error) { + } catch (error) { console.error('Error picking image;', error); Alert.alert('Erreur', "Une erreur est survenue lors de la sélection d'une image."); } - }; + }, []); - function sendImage(location) { - 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}; + return {image, pickImage}; }; diff --git a/mobile/traque-app/hook/useSendDeviceInfo.jsx b/mobile/traque-app/hook/useSendDeviceInfo.jsx index 868b46e..ab1be39 100644 --- a/mobile/traque-app/hook/useSendDeviceInfo.jsx +++ b/mobile/traque-app/hook/useSendDeviceInfo.jsx @@ -1,35 +1,47 @@ -import { useEffect } from 'react'; +// React +import { useEffect, useRef } from 'react'; import DeviceInfo from 'react-native-device-info'; -import { useSocket } from "../context/socketContext"; +// Context import { useTeamConnexion } from "../context/teamConnexionContext"; +// Hook +import { useSocketCommands } from "./useSocketCommands"; export const useSendDeviceInfo = () => { - const batteryUpdateTimeout = 5*60*1000; - const { teamSocket } = useSocket(); - const {loggedIn} = useTeamConnexion(); + const { emitBattery, emitDeviceInfo } = useSocketCommands(); + const { loggedIn } = useTeamConnexion(); + const isMounted = useRef(true); useEffect(() => { + isMounted.current = true; + if (!loggedIn) return; const sendInfo = async () => { - const brand = DeviceInfo.getBrand(); - const model = DeviceInfo.getModel(); - const name = await DeviceInfo.getDeviceName(); - teamSocket.emit('device_info', {model: brand + " " + model, name: name}); + const [brand, model, name] = await Promise.all([ + DeviceInfo.getBrand(), + DeviceInfo.getModel(), + DeviceInfo.getDeviceName() + ]); + if (!isMounted) return; + emitDeviceInfo({model: brand + " " + model, name: name}); }; const sendBattery = async () => { const level = await DeviceInfo.getBatteryLevel(); - teamSocket.emit('battery_update', Math.round(level * 100)); + if (!isMounted) return; + emitBattery(Math.round(level * 100)); }; sendInfo(); sendBattery(); - const batteryCheckInterval = setInterval(() => sendBattery(), batteryUpdateTimeout); + const batteryCheckInterval = setInterval(() => sendBattery(), 5*60*1000); // 5 minutes - return () => clearInterval(batteryCheckInterval); - }, [batteryUpdateTimeout, loggedIn, teamSocket]); + return () => { + isMounted.current = false; + clearInterval(batteryCheckInterval); + }; + }, [emitBattery, emitDeviceInfo, loggedIn]); return null; }; diff --git a/mobile/traque-app/hook/useSocketAuth.jsx b/mobile/traque-app/hook/useSocketAuth.jsx index 267c598..5efebaf 100644 --- a/mobile/traque-app/hook/useSocketAuth.jsx +++ b/mobile/traque-app/hook/useSocketAuth.jsx @@ -1,72 +1,60 @@ -import { useEffect, useState } from 'react'; +// React +import { useState, useEffect, useCallback, useRef } from 'react'; +// Hook import { useLocalStorage } from './useLocalStorage'; +import { useSocketCommands } from "./useSocketCommands"; -const LOGIN_MESSAGE = "login"; -const LOGOUT_MESSAGE = "logout"; - -export const useSocketAuth = (socket, passwordName) => { +export const useSocketAuth = () => { + const { emitLogin, emitLogout } = useSocketCommands(); const [loggedIn, setLoggedIn] = useState(false); - const [loading, setLoading] = useState(true); - const [waitingForResponse, setWaitingForResponse] = useState(false); - const [hasTriedSavedPassword, setHasTriedSavedPassword] = useState(false); - const [savedPassword, setSavedPassword, savedPasswordLoading] = useLocalStorage(passwordName, null); + const [savedPassword, setSavedPassword] = useLocalStorage("team_password", null); + const isMounted = useRef(true); useEffect(() => { - if (!loading && !hasTriedSavedPassword) { - console.log("Try to log in with saved password :", savedPassword); - setWaitingForResponse(true); + isMounted.current = true; + return () => { + 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) => { const timeout = setTimeout(() => { + if (!isMounted.current) return; console.warn("Server did not respond to login emit."); - setWaitingForResponse(false); reject(); - }, 3000); + }, 2000); - socket.emit(LOGIN_MESSAGE, password, (response) => { + emitLogin(password, (response) => { clearTimeout(timeout); - console.log(response.message); - setLoggedIn(response.isLoggedIn); - setWaitingForResponse(false); - resolve(response); + + if (!isMounted.current) return; + + if (response.isLoggedIn) { + console.log("Log in accepted."); + setLoggedIn(true); + setSavedPassword(password); + resolve(response); + } else { + console.log("Log in refused."); + setLoggedIn(false); + reject(); + } }); }); - } - - function logout() { - console.log("Logout"); - setSavedPassword(null); - setLoggedIn(false); - socket.emit(LOGOUT_MESSAGE); - } + }, [emitLogin, setSavedPassword]); useEffect(() => { - if(!waitingForResponse && !savedPasswordLoading) { - setLoading(false); - } else { - setLoading(true); + if (!loggedIn && savedPassword) { + login(savedPassword); } - }, [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}; }; diff --git a/mobile/traque-app/hook/useSocketCommands.jsx b/mobile/traque-app/hook/useSocketCommands.jsx new file mode 100644 index 0000000..0f9ae6d --- /dev/null +++ b/mobile/traque-app/hook/useSocketCommands.jsx @@ -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 }; +}; diff --git a/mobile/traque-app/hook/useSocketListener.jsx b/mobile/traque-app/hook/useSocketListener.jsx index 2d20298..55c86b6 100644 --- a/mobile/traque-app/hook/useSocketListener.jsx +++ b/mobile/traque-app/hook/useSocketListener.jsx @@ -1,8 +1,9 @@ +// React import { useEffect } from "react"; export const useSocketListener = (socket, event, callback) => { useEffect(() => { - socket.on(event,callback); + socket.on(event, callback); return () => { socket.off(event, callback); }; diff --git a/mobile/traque-app/hook/useTimeDifference.jsx b/mobile/traque-app/hook/useTimeDifference.jsx index 9ee818b..c42d2fe 100644 --- a/mobile/traque-app/hook/useTimeDifference.jsx +++ b/mobile/traque-app/hook/useTimeDifference.jsx @@ -1,3 +1,4 @@ +// React import { useEffect, useState } from "react"; export const useTimeDifference = (refTime, timeout) => { diff --git a/mobile/traque-app/util/constants.js b/mobile/traque-app/util/constants.js index ec0be5e..cabfae7 100644 --- a/mobile/traque-app/util/constants.js +++ b/mobile/traque-app/util/constants.js @@ -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 = { paris : { latitude: 48.864, @@ -10,4 +13,4 @@ export const InitialRegions = { export const ZoneTypes = { circle: "circle", polygon: "polygon" -}; \ No newline at end of file +}; diff --git a/server/traque-back/game.js b/server/traque-back/game.js index c9a382c..f2a7c12 100644 --- a/server/traque-back/game.js +++ b/server/traque-back/game.js @@ -56,7 +56,7 @@ export default { getNewTeamId() { let 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() { diff --git a/server/traque-back/photo.js b/server/traque-back/photo.js index d26c20b..e53f723 100644 --- a/server/traque-back/photo.js +++ b/server/traque-back/photo.js @@ -31,7 +31,7 @@ const upload = multer({ fileFilter: function (req, file, callback) { if (ALLOWED_MIME.indexOf(file.mimetype) == -1) { callback(null, false); - } else if (!game.getTeam(Number(req.query.team))) { + } else if (!game.getTeam(req.query.team)) { callback(null, false); } else { callback(null, true); @@ -58,9 +58,9 @@ export function initPhotoUpload() { }) //App handler for serving the photo of a team given its secret ID app.get("/photo/my", (req, res) => { - let team = game.getTeam(Number(req.query.team)); + let team = game.getTeam(req.query.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("Access-Control-Allow-Origin", "*"); 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.get("/photo/enemy", (req, res) => { - let team = game.getTeam(Number(req.query.team)); + let team = game.getTeam(req.query.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("Access-Control-Allow-Origin", "*"); res.sendFile(fs.existsSync(imagePath) ? imagePath : path.join(process.cwd(), "images", "missing_image.jpg"));