2021-01-22 14:59:05 +11:00
|
|
|
import React, {
|
|
|
|
|
useEffect,
|
|
|
|
|
useState,
|
|
|
|
|
useContext,
|
|
|
|
|
useCallback,
|
|
|
|
|
useRef,
|
|
|
|
|
} from "react";
|
2020-11-26 16:29:10 +11:00
|
|
|
import * as Comlink from "comlink";
|
2021-03-16 17:49:50 +11:00
|
|
|
import { decode, encode } from "@msgpack/msgpack";
|
2020-05-19 16:21:01 +10:00
|
|
|
|
2021-02-06 13:32:38 +11:00
|
|
|
import { useAuth } from "./AuthContext";
|
|
|
|
|
import { useDatabase } from "./DatabaseContext";
|
2020-05-19 16:21:01 +10:00
|
|
|
|
|
|
|
|
import { maps as defaultMaps } from "../maps";
|
|
|
|
|
|
|
|
|
|
const MapDataContext = React.createContext();
|
|
|
|
|
|
2020-09-11 16:56:40 +10:00
|
|
|
// Maximum number of maps to keep in the cache
|
|
|
|
|
const cachedMapMax = 15;
|
|
|
|
|
|
2020-05-19 16:21:01 +10:00
|
|
|
const defaultMapState = {
|
|
|
|
|
tokens: {},
|
2021-02-04 09:11:27 +11:00
|
|
|
drawShapes: {},
|
|
|
|
|
fogShapes: {},
|
2020-05-19 16:21:01 +10:00
|
|
|
// Flags to determine what other people can edit
|
2020-11-05 16:21:52 +11:00
|
|
|
editFlags: ["drawing", "tokens", "notes"],
|
2020-11-04 15:02:56 +11:00
|
|
|
notes: {},
|
2020-05-19 16:21:01 +10:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function MapDataProvider({ children }) {
|
2021-03-19 15:32:17 +11:00
|
|
|
const { database, databaseStatus, worker } = useDatabase();
|
2021-02-06 13:32:38 +11:00
|
|
|
const { userId } = useAuth();
|
2020-05-19 16:21:01 +10:00
|
|
|
|
|
|
|
|
const [maps, setMaps] = useState([]);
|
|
|
|
|
const [mapStates, setMapStates] = useState([]);
|
2020-11-26 16:29:10 +11:00
|
|
|
const [mapsLoading, setMapsLoading] = useState(true);
|
|
|
|
|
|
2020-05-19 16:21:01 +10:00
|
|
|
// Load maps from the database and ensure state is properly setup
|
|
|
|
|
useEffect(() => {
|
2020-10-23 22:16:18 +11:00
|
|
|
if (!userId || !database || databaseStatus === "loading") {
|
2020-05-19 16:21:01 +10:00
|
|
|
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,
|
2020-05-31 16:25:05 +10:00
|
|
|
showGrid: false,
|
2020-08-07 12:28:50 +10:00
|
|
|
snapToGrid: true,
|
2020-10-01 15:05:30 +10:00
|
|
|
group: "default",
|
2020-05-19 16:21:01 +10:00
|
|
|
});
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-08 16:53:56 +11:00
|
|
|
// Loads maps without the file data to save memory
|
2020-05-19 16:21:01 +10:00
|
|
|
async function loadMaps() {
|
2021-01-27 16:24:13 +11:00
|
|
|
let storedMaps = [];
|
|
|
|
|
// Try to load maps with worker, fallback to database if failed
|
|
|
|
|
const packedMaps = await worker.loadData("maps");
|
2021-03-19 15:32:17 +11:00
|
|
|
// let packedMaps;
|
2021-01-27 16:24:13 +11:00
|
|
|
if (packedMaps) {
|
|
|
|
|
storedMaps = decode(packedMaps);
|
|
|
|
|
} else {
|
|
|
|
|
console.warn("Unable to load maps with worker, loading may be slow");
|
2021-02-08 16:53:56 +11:00
|
|
|
await database.table("maps").each((map) => {
|
|
|
|
|
const { file, resolutions, ...rest } = map;
|
|
|
|
|
storedMaps.push(rest);
|
|
|
|
|
});
|
2021-01-27 16:24:13 +11:00
|
|
|
}
|
2020-05-19 16:21:01 +10:00
|
|
|
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);
|
2020-11-26 16:29:10 +11:00
|
|
|
setMapsLoading(false);
|
2020-05-19 16:21:01 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loadMaps();
|
2021-03-19 15:32:17 +11:00
|
|
|
}, [userId, database, databaseStatus, worker]);
|
2020-05-19 16:21:01 +10:00
|
|
|
|
2021-01-22 14:59:05 +11:00
|
|
|
const mapsRef = useRef(maps);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
mapsRef.current = maps;
|
|
|
|
|
}, [maps]);
|
2020-05-19 16:21:01 +10:00
|
|
|
|
2021-01-22 14:59:05 +11:00
|
|
|
const getMap = useCallback((mapId) => {
|
|
|
|
|
return mapsRef.current.find((map) => map.id === mapId);
|
|
|
|
|
}, []);
|
2020-10-01 22:32:21 +10:00
|
|
|
|
2021-01-22 14:59:05 +11:00
|
|
|
const getMapFromDB = useCallback(
|
|
|
|
|
async (mapId) => {
|
|
|
|
|
let map = await database.table("maps").get(mapId);
|
|
|
|
|
return map;
|
|
|
|
|
},
|
|
|
|
|
[database]
|
|
|
|
|
);
|
2020-09-11 16:56:40 +10:00
|
|
|
|
2021-02-14 18:35:42 +11:00
|
|
|
const getMapStateFromDB = useCallback(
|
|
|
|
|
async (mapId) => {
|
|
|
|
|
let mapState = await database.table("states").get(mapId);
|
|
|
|
|
return mapState;
|
|
|
|
|
},
|
|
|
|
|
[database]
|
|
|
|
|
);
|
|
|
|
|
|
2020-09-11 16:56:40 +10:00
|
|
|
/**
|
|
|
|
|
* Keep up to cachedMapMax amount of maps that you don't own
|
|
|
|
|
* Sorted by when they we're last used
|
|
|
|
|
*/
|
2021-01-22 14:59:05 +11:00
|
|
|
const updateCache = useCallback(async () => {
|
2020-09-11 16:56:40 +10:00
|
|
|
const cachedMaps = await database
|
|
|
|
|
.table("maps")
|
|
|
|
|
.where("owner")
|
|
|
|
|
.notEqual(userId)
|
|
|
|
|
.sortBy("lastUsed");
|
|
|
|
|
if (cachedMaps.length > cachedMapMax) {
|
|
|
|
|
const cacheDeleteCount = cachedMaps.length - cachedMapMax;
|
|
|
|
|
const idsToDelete = cachedMaps
|
|
|
|
|
.slice(0, cacheDeleteCount)
|
|
|
|
|
.map((map) => map.id);
|
|
|
|
|
database.table("maps").where("id").anyOf(idsToDelete).delete();
|
|
|
|
|
}
|
2021-01-22 14:59:05 +11:00
|
|
|
}, [database, userId]);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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) => {
|
2021-02-14 18:35:42 +11:00
|
|
|
// Just update map database as react state will be updated with an Observable
|
2021-01-22 14:59:05 +11:00
|
|
|
const state = { ...defaultMapState, mapId: map.id };
|
2021-02-14 18:35:42 +11:00
|
|
|
await database.table("maps").add(map);
|
2021-01-22 14:59:05 +11:00
|
|
|
await database.table("states").add(state);
|
|
|
|
|
if (map.owner !== userId) {
|
|
|
|
|
await updateCache();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[database, updateCache, userId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const removeMap = useCallback(
|
|
|
|
|
async (id) => {
|
|
|
|
|
await database.table("maps").delete(id);
|
|
|
|
|
await database.table("states").delete(id);
|
|
|
|
|
},
|
|
|
|
|
[database]
|
|
|
|
|
);
|
2020-05-19 22:15:08 +10:00
|
|
|
|
2021-01-22 14:59:05 +11:00
|
|
|
const removeMaps = useCallback(
|
|
|
|
|
async (ids) => {
|
|
|
|
|
await database.table("maps").bulkDelete(ids);
|
|
|
|
|
await database.table("states").bulkDelete(ids);
|
|
|
|
|
},
|
|
|
|
|
[database]
|
|
|
|
|
);
|
2020-05-19 22:15:08 +10:00
|
|
|
|
2021-01-22 14:59:05 +11:00
|
|
|
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) => {
|
|
|
|
|
// fake-indexeddb throws an error when updating maps in production.
|
|
|
|
|
// Catch that error and use put when it fails
|
|
|
|
|
try {
|
|
|
|
|
await database.table("maps").update(id, update);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const map = (await getMapFromDB(id)) || {};
|
|
|
|
|
await database.table("maps").put({ ...map, id, ...update });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[database, getMapFromDB]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Adds a map to the database if none exists or replaces a map if it already exists
|
|
|
|
|
* Note: this does not add a map state to do that use AddMap
|
|
|
|
|
* @param {Object} map the map to put
|
|
|
|
|
*/
|
|
|
|
|
const putMap = useCallback(
|
|
|
|
|
async (map) => {
|
2021-03-16 17:49:50 +11:00
|
|
|
// Attempt to use worker to put map to avoid UI lockup
|
|
|
|
|
const packedMap = encode(map);
|
|
|
|
|
const success = await worker.putData(
|
|
|
|
|
Comlink.transfer(packedMap, [packedMap.buffer]),
|
|
|
|
|
"maps",
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
if (!success) {
|
|
|
|
|
await database.table("maps").put(map);
|
|
|
|
|
}
|
2021-01-22 14:59:05 +11:00
|
|
|
if (map.owner !== userId) {
|
|
|
|
|
await updateCache();
|
|
|
|
|
}
|
|
|
|
|
},
|
2021-03-19 15:32:17 +11:00
|
|
|
[database, updateCache, userId, worker]
|
2021-01-22 14:59:05 +11:00
|
|
|
);
|
2020-08-28 17:06:13 +10:00
|
|
|
|
2021-02-14 18:35:42 +11:00
|
|
|
// 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]);
|
2021-02-16 18:31:24 +11:00
|
|
|
} 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;
|
|
|
|
|
});
|
2021-02-14 18:35:42 +11:00
|
|
|
} 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;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-02-16 18:31:24 +11:00
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-02-14 18:35:42 +11:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
database.on("changes", handleMapChanges);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
database.on("changes").unsubscribe(handleMapChanges);
|
|
|
|
|
};
|
|
|
|
|
}, [database, databaseStatus]);
|
|
|
|
|
|
2020-05-19 22:15:08 +10:00
|
|
|
const ownedMaps = maps.filter((map) => map.owner === userId);
|
|
|
|
|
|
2020-05-19 16:21:01 +10:00
|
|
|
const value = {
|
|
|
|
|
maps,
|
2020-05-19 22:15:08 +10:00
|
|
|
ownedMaps,
|
2020-05-19 16:21:01 +10:00
|
|
|
mapStates,
|
|
|
|
|
addMap,
|
|
|
|
|
removeMap,
|
2020-09-30 15:44:48 +10:00
|
|
|
removeMaps,
|
2020-05-19 16:21:01 +10:00
|
|
|
resetMap,
|
|
|
|
|
updateMap,
|
2020-10-01 22:32:21 +10:00
|
|
|
updateMaps,
|
2020-05-19 16:21:01 +10:00
|
|
|
updateMapState,
|
2020-05-19 22:15:08 +10:00
|
|
|
putMap,
|
|
|
|
|
getMap,
|
2020-08-28 17:06:13 +10:00
|
|
|
getMapFromDB,
|
2020-11-26 16:29:10 +11:00
|
|
|
mapsLoading,
|
2021-02-14 18:35:42 +11:00
|
|
|
getMapStateFromDB,
|
2020-05-19 16:21:01 +10:00
|
|
|
};
|
|
|
|
|
return (
|
|
|
|
|
<MapDataContext.Provider value={value}>{children}</MapDataContext.Provider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-06 13:32:38 +11:00
|
|
|
export function useMapData() {
|
|
|
|
|
const context = useContext(MapDataContext);
|
|
|
|
|
if (context === undefined) {
|
|
|
|
|
throw new Error("useMapData must be used within a MapDataProvider");
|
|
|
|
|
}
|
|
|
|
|
return context;
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-19 16:21:01 +10:00
|
|
|
export default MapDataContext;
|