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

View File

@@ -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 };
};

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 { 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];
};

View File

@@ -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];
};

View File

@@ -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};
};

View File

@@ -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;
};

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 { 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};
};

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,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);
};

View File

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