import React, { useEffect, useState, useContext, useCallback } from "react"; import { decode } from "@msgpack/msgpack"; import { useAuth } from "./AuthContext"; import { useDatabase } from "./DatabaseContext"; import { maps as defaultMaps } from "../maps"; const MapDataContext = React.createContext(); const defaultMapState = { tokens: {}, drawShapes: {}, fogShapes: {}, // Flags to determine what other people can edit editFlags: ["drawing", "tokens", "notes"], notes: {}, }; export function MapDataProvider({ children }) { const { database, databaseStatus, worker } = useDatabase(); const { userId } = useAuth(); const [maps, setMaps] = useState([]); const [mapStates, setMapStates] = useState([]); const [mapsLoading, setMapsLoading] = useState(true); // Load maps from the database and ensure state is properly setup useEffect(() => { if (!userId || !database || databaseStatus === "loading") { return; } async function getDefaultMaps() { const defaultMapsWithIds = []; for (let i = 0; i < defaultMaps.length; i++) { const defaultMap = defaultMaps[i]; const id = `__default-${defaultMap.name}`; defaultMapsWithIds.push({ ...defaultMap, id, owner: userId, // Emulate the time increasing to avoid sort errors created: Date.now() + i, lastModified: Date.now() + i, showGrid: false, snapToGrid: true, group: "default", }); // Add a state for the map if there isn't one already const state = await database.table("states").get(id); if (!state) { await database.table("states").add({ ...defaultMapState, mapId: id }); } } return defaultMapsWithIds; } async function loadMaps() { let storedMaps = []; // Try to load maps with worker, fallback to database if failed const packedMaps = await worker.loadData("maps"); // let packedMaps; if (packedMaps) { storedMaps = decode(packedMaps); } else { console.warn("Unable to load maps with worker, loading may be slow"); await database.table("maps").each((map) => { storedMaps.push(map); }); } const sortedMaps = storedMaps.sort((a, b) => b.created - a.created); const defaultMapsWithIds = await getDefaultMaps(); const allMaps = [...sortedMaps, ...defaultMapsWithIds]; setMaps(allMaps); const storedStates = await database.table("states").toArray(); setMapStates(storedStates); setMapsLoading(false); } loadMaps(); }, [userId, database, databaseStatus, worker]); const getMap = useCallback( async (mapId) => { let map = await database.table("maps").get(mapId); return map; }, [database] ); const getMapState = useCallback( async (mapId) => { let mapState = await database.table("states").get(mapId); return mapState; }, [database] ); /** * Adds a map to the database, also adds an assosiated state for that map * @param {Object} map map to add */ const addMap = useCallback( async (map) => { // Just update map database as react state will be updated with an Observable const state = { ...defaultMapState, mapId: map.id }; await database.table("maps").add(map); await database.table("states").add(state); }, [database] ); const removeMaps = useCallback( async (ids) => { const maps = await database.table("maps").bulkGet(ids); // Remove assets linked with maps let assetIds = []; for (let map of maps) { if (map.type === "file") { assetIds.push(map.file); assetIds.push(map.thumbnail); for (let res of Object.values(map.resolutions)) { assetIds.push(res); } } } await database.table("maps").bulkDelete(ids); await database.table("states").bulkDelete(ids); await database.table("assets").bulkDelete(assetIds); }, [database] ); const resetMap = useCallback( async (id) => { const state = { ...defaultMapState, mapId: id }; await database.table("states").put(state); return state; }, [database] ); const updateMap = useCallback( async (id, update) => { await database.table("maps").update(id, update); }, [database] ); const updateMaps = useCallback( async (ids, update) => { await Promise.all( ids.map((id) => database.table("maps").update(id, update)) ); }, [database] ); const updateMapState = useCallback( async (id, update) => { await database.table("states").update(id, update); }, [database] ); // Create DB observable to sync creating and deleting useEffect(() => { if (!database || databaseStatus === "loading") { return; } function handleMapChanges(changes) { for (let change of changes) { if (change.table === "maps") { if (change.type === 1) { // Created const map = change.obj; const state = { ...defaultMapState, mapId: map.id }; setMaps((prevMaps) => [map, ...prevMaps]); setMapStates((prevStates) => [state, ...prevStates]); } else if (change.type === 2) { const map = change.obj; setMaps((prevMaps) => { const newMaps = [...prevMaps]; const i = newMaps.findIndex((m) => m.id === map.id); if (i > -1) { newMaps[i] = map; } return newMaps; }); } else if (change.type === 3) { // Deleted const id = change.key; setMaps((prevMaps) => { const filtered = prevMaps.filter((map) => map.id !== id); return filtered; }); setMapStates((prevMapsStates) => { const filtered = prevMapsStates.filter( (state) => state.mapId !== id ); return filtered; }); } } if (change.table === "states") { if (change.type === 2) { // Update map state const state = change.obj; setMapStates((prevMapStates) => { const newStates = [...prevMapStates]; const i = newStates.findIndex((s) => s.mapId === state.mapId); if (i > -1) { newStates[i] = state; } return newStates; }); } } } } database.on("changes", handleMapChanges); return () => { database.on("changes").unsubscribe(handleMapChanges); }; }, [database, databaseStatus]); const ownedMaps = maps.filter((map) => map.owner === userId); const value = { maps, ownedMaps, mapStates, addMap, removeMaps, resetMap, updateMap, updateMaps, updateMapState, getMap, mapsLoading, getMapState, }; return ( {children} ); } export function useMapData() { const context = useContext(MapDataContext); if (context === undefined) { throw new Error("useMapData must be used within a MapDataProvider"); } return context; } export default MapDataContext;