Merge branch 'bugfix/map-send' into release/v1.2.1

This commit is contained in:
Mitchell McCaffrey 2020-05-04 00:13:39 +10:00
commit 97d99a3ea1
19 changed files with 385 additions and 194 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
REACT_APP_BROKER_URL=http://localhost:9000
REACT_APP_ICE_SERVERS_URL=http://localhost:9000/iceservers

2
.env.production Normal file
View File

@ -0,0 +1,2 @@
REACT_APP_BROKER_URL=https://agent.owlbear.rodeo
REACT_APP_ICE_SERVERS_URL=https://agent.owlbear.rodeo/iceservers

View File

@ -3,18 +3,20 @@
"version": "1.2.0",
"private": true,
"dependencies": {
"@msgpack/msgpack": "^1.12.1",
"@stripe/stripe-js": "^1.3.2",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"dexie": "^2.0.4",
"fake-indexeddb": "^3.0.0",
"interactjs": "^1.9.7",
"js-binarypack": "^0.0.9",
"normalize-wheel": "^1.0.1",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-modal": "^3.11.2",
"react-router-dom": "^5.1.2",
"react-router-hash-link": "^1.2.2",
"react-scripts": "3.4.0",
"shortid": "^2.2.15",
"simple-peer": "^9.6.2",

View File

@ -9,28 +9,31 @@ import About from "./routes/About";
import FAQ from "./routes/FAQ";
import { AuthProvider } from "./contexts/AuthContext";
import { DatabaseProvider } from "./contexts/DatabaseContext";
function App() {
return (
<ThemeProvider theme={theme}>
<AuthProvider>
<Router>
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/faq">
<FAQ />
</Route>
<Route path="/game/:id">
<Game />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</Router>
</AuthProvider>
<DatabaseProvider>
<AuthProvider>
<Router>
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/faq">
<FAQ />
</Route>
<Route path="/game/:id">
<Game />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</Router>
</AuthProvider>
</DatabaseProvider>
</ThemeProvider>
);
}

View File

@ -1,6 +1,6 @@
import React from "react";
import { Link as ThemeLink } from "theme-ui";
import { Link as RouterLink } from "react-router-dom";
import { HashLink as RouterLink } from "react-router-hash-link";
function Link({ to, ...rest }) {
return (

View File

@ -1,10 +1,13 @@
import React from "react";
import { Flex } from "theme-ui";
import React, { useContext } from "react";
import { Flex, Box, Text } from "theme-ui";
import SimpleBar from "simplebar-react";
import AddIcon from "../../icons/AddIcon";
import MapTile from "./MapTile";
import Link from "../Link";
import DatabaseContext from "../../contexts/DatabaseContext";
function MapTiles({
maps,
@ -16,59 +19,80 @@ function MapTiles({
onMapReset,
onSubmit,
}) {
const { databaseStatus } = useContext(DatabaseContext);
return (
<SimpleBar style={{ maxHeight: "300px", width: "500px" }}>
<Flex
py={2}
bg="muted"
sx={{
flexWrap: "wrap",
width: "500px",
borderRadius: "4px",
}}
>
<Box sx={{ position: "relative" }}>
<SimpleBar style={{ maxHeight: "300px", width: "500px" }}>
<Flex
onClick={onMapAdd}
sx={{
":hover": {
color: "primary",
},
":focus": {
outline: "none",
},
":active": {
color: "secondary",
},
width: "150px",
height: "150px",
borderRadius: "4px",
justifyContent: "center",
alignItems: "center",
cursor: "pointer",
}}
m={2}
py={2}
bg="muted"
aria-label="Add Map"
title="Add Map"
sx={{
flexWrap: "wrap",
width: "500px",
borderRadius: "4px",
}}
>
<AddIcon large />
<Flex
onClick={onMapAdd}
sx={{
":hover": {
color: "primary",
},
":focus": {
outline: "none",
},
":active": {
color: "secondary",
},
width: "150px",
height: "150px",
borderRadius: "4px",
justifyContent: "center",
alignItems: "center",
cursor: "pointer",
}}
m={2}
bg="muted"
aria-label="Add Map"
title="Add Map"
>
<AddIcon large />
</Flex>
{maps.map((map) => (
<MapTile
key={map.id}
map={map}
mapState={
selectedMap && map.id === selectedMap.id && selectedMapState
}
isSelected={selectedMap && map.id === selectedMap.id}
onMapSelect={onMapSelect}
onMapRemove={onMapRemove}
onMapReset={onMapReset}
onSubmit={onSubmit}
/>
))}
</Flex>
{maps.map((map) => (
<MapTile
key={map.id}
map={map}
mapState={
selectedMap && map.id === selectedMap.id && selectedMapState
}
isSelected={selectedMap && map.id === selectedMap.id}
onMapSelect={onMapSelect}
onMapRemove={onMapRemove}
onMapReset={onMapReset}
onSubmit={onSubmit}
/>
))}
</Flex>
</SimpleBar>
</SimpleBar>
{databaseStatus === "disabled" && (
<Box
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
textAlign: "center",
}}
bg="highlight"
p={1}
>
<Text as="p" variant="body2">
Map saving is unavailable. See <Link to="/faq#saving">FAQ</Link> for
more information.
</Text>
</Box>
)}
</Box>
);
}

View File

@ -22,7 +22,7 @@ function StartStreamButton({ onStreamStart, onStreamEnd, stream }) {
Browser not supported for audio sharing.
<br />
<br />
See <Link to="/faq">FAQ</Link> for more information.
See <Link to="/faq#radio">FAQ</Link> for more information.
</Text>
</Box>
);
@ -35,7 +35,7 @@ function StartStreamButton({ onStreamStart, onStreamEnd, stream }) {
Ensure "Share audio" is selected when sharing.
<br />
<br />
See <Link to="/faq">FAQ</Link> for more information.
See <Link to="/faq#radio">FAQ</Link> for more information.
</Text>
</Box>
);

View File

@ -1,13 +1,15 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useContext } from "react";
import shortid from "shortid";
import { getRandomMonster } from "../helpers/monsters";
import DatabaseContext from "./DatabaseContext";
import db from "../database";
import { getRandomMonster } from "../helpers/monsters";
const AuthContext = React.createContext();
export function AuthProvider({ children }) {
const { database } = useContext(DatabaseContext);
const [password, setPassword] = useState(
sessionStorage.getItem("auth") || ""
);
@ -20,41 +22,47 @@ export function AuthProvider({ children }) {
const [userId, setUserId] = useState();
useEffect(() => {
if (!database) {
return;
}
async function loadUserId() {
const storedUserId = await db.table("user").get("userId");
const storedUserId = await database.table("user").get("userId");
if (storedUserId) {
setUserId(storedUserId.value);
} else {
const id = shortid.generate();
setUserId(id);
db.table("user").add({ key: "userId", value: id });
database.table("user").add({ key: "userId", value: id });
}
}
loadUserId();
}, []);
}, [database]);
const [nickname, setNickname] = useState("");
useEffect(() => {
if (!database) {
return;
}
async function loadNickname() {
const storedNickname = await db.table("user").get("nickname");
const storedNickname = await database.table("user").get("nickname");
if (storedNickname) {
setNickname(storedNickname.value);
} else {
const name = getRandomMonster();
setNickname(name);
db.table("user").add({ key: "nickname", value: name });
database.table("user").add({ key: "nickname", value: name });
}
}
loadNickname();
}, []);
}, [database]);
useEffect(() => {
if (nickname !== undefined) {
db.table("user").update("nickname", { value: nickname });
if (nickname !== undefined && database !== undefined) {
database.table("user").update("nickname", { value: nickname });
}
}, [nickname]);
}, [nickname, database]);
const value = {
userId,

View File

@ -0,0 +1,44 @@
import React, { useState, useEffect } from "react";
import { getDatabase } from "../database";
const DatabaseContext = React.createContext();
export function DatabaseProvider({ children }) {
const [database, setDatabase] = useState();
const [databaseStatus, setDatabaseStatus] = useState("loading");
useEffect(() => {
// Create a test database and open it to see if indexedDB is enabled
let testDBRequest = window.indexedDB.open("__test");
testDBRequest.onsuccess = function () {
testDBRequest.result.close();
let db = getDatabase();
setDatabase(db);
setDatabaseStatus("loaded");
window.indexedDB.deleteDatabase("__test");
};
// If indexedb disabled create an in memory database
testDBRequest.onerror = async function () {
console.warn("Database is disabled, no state will be saved");
const indexedDB = await import("fake-indexeddb");
const IDBKeyRange = await import("fake-indexeddb/lib/FDBKeyRange");
let db = getDatabase({ indexedDB, IDBKeyRange });
setDatabase(db);
setDatabaseStatus("disabled");
window.indexedDB.deleteDatabase("__test");
};
}, []);
const value = {
database,
databaseStatus,
};
return (
<DatabaseContext.Provider value={value}>
{children}
</DatabaseContext.Provider>
);
}
export default DatabaseContext;

View File

@ -1,11 +1,36 @@
import Dexie from "dexie";
const db = new Dexie("OwlbearRodeoDB");
db.version(1).stores({
maps: "id, owner",
states: "mapId",
tokens: "id, owner",
user: "key",
});
import blobToBuffer from "./helpers/blobToBuffer";
export default db;
function loadVersions(db) {
// v1.2.0
db.version(1).stores({
maps: "id, owner",
states: "mapId",
tokens: "id, owner",
user: "key",
});
// v1.2.1 - Move from blob files to array buffers
db.version(2)
.stores({})
.upgrade(async (tx) => {
const maps = await Dexie.waitFor(tx.table("maps").toArray());
let mapBuffers = {};
for (let map of maps) {
mapBuffers[map.id] = await Dexie.waitFor(blobToBuffer(map.file));
}
return tx
.table("maps")
.toCollection()
.modify((map) => {
map.file = mapBuffers[map.id];
});
});
}
// Get the dexie database used in DatabaseContext
export function getDatabase(options) {
let db = new Dexie("OwlbearRodeoDB", options);
loadVersions(db);
return db;
}

View File

@ -1,5 +1,5 @@
import SimplePeer from "simple-peer";
import BinaryPack from "js-binarypack";
import { encode, decode } from "@msgpack/msgpack";
import shortid from "shortid";
import blobToBuffer from "./blobToBuffer";
@ -14,7 +14,7 @@ class Peer extends SimplePeer {
this.currentChunks = {};
this.on("data", (packed) => {
const unpacked = BinaryPack.unpack(packed);
const unpacked = decode(packed);
// If the special property __chunked is set and true
// The data is a partial chunk of the a larger file
// So wait until all chunks are collected and assembled
@ -31,9 +31,13 @@ class Peer extends SimplePeer {
// All chunks have been loaded
if (chunk.count === chunk.total) {
const merged = BinaryPack.unpack(Buffer.concat(chunk.data));
this.emit("dataComplete", merged);
delete this.currentChunks[unpacked.id];
// Merge chunks with a blob
// TODO: Look at a more efficient way to recombine buffer data
const merged = new Blob(chunk.data);
blobToBuffer(merged).then((buffer) => {
this.emit("dataComplete", decode(buffer));
delete this.currentChunks[unpacked.id];
});
}
} else {
this.emit("dataComplete", unpacked);
@ -41,29 +45,24 @@ class Peer extends SimplePeer {
});
}
async sendPackedData(packedData) {
const buffer = await blobToBuffer(packedData);
super.send(buffer);
}
send(data) {
const packedData = BinaryPack.pack(data);
const packedData = encode(data);
if (packedData.size > MAX_BUFFER_SIZE) {
if (packedData.byteLength > MAX_BUFFER_SIZE) {
const chunks = this.chunk(packedData);
for (let chunk of chunks) {
this.sendPackedData(BinaryPack.pack(chunk));
super.send(encode(chunk));
}
return;
} else {
this.sendPackedData(packedData);
super.send(packedData);
}
}
// Converted from https://github.com/peers/peerjs/
chunk(blob) {
chunk(data) {
const chunks = [];
const size = blob.size;
const size = data.byteLength;
const total = Math.ceil(size / MAX_BUFFER_SIZE);
const id = shortid.generate();
@ -72,7 +71,7 @@ class Peer extends SimplePeer {
while (start < size) {
const end = Math.min(size, start + MAX_BUFFER_SIZE);
const slice = blob.slice(start, end);
const slice = data.slice(start, end);
const chunk = {
__chunked: true,

View File

@ -1,12 +1,10 @@
async function blobToBuffer(blob) {
if (blob.arrayBuffer) {
const arrayBuffer = await blob.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
return buffer;
return new Uint8Array(arrayBuffer);
} else {
const arrayBuffer = await new Response(blob).arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
return buffer;
const arrayBuffer = new Response(blob).arrayBuffer();
return new Uint8Array(arrayBuffer);
}
}

View File

@ -11,7 +11,7 @@ function useDataSource(data, defaultSources) {
}
let url = null;
if (data.type === "file") {
url = URL.createObjectURL(data.file);
url = URL.createObjectURL(new Blob([data.file]));
} else if (data.type === "default") {
url = defaultSources[data.key];
}

View File

@ -6,7 +6,7 @@ import Peer from "../helpers/Peer";
import AuthContext from "../contexts/AuthContext";
const socket = io("https://agent.owlbear.rodeo");
const socket = io(process.env.REACT_APP_BROKER_URL);
function useSession(
partyId,
@ -18,9 +18,16 @@ function useSession(
onPeerError
) {
const { password, setAuthenticationStatus } = useContext(AuthContext);
const [iceServers, setIceServers] = useState([]);
useEffect(() => {
socket.emit("join party", partyId, password);
async function joinParty() {
const response = await fetch(process.env.REACT_APP_ICE_SERVERS_URL);
const data = await response.json();
setIceServers(data.iceServers);
socket.emit("join party", partyId, password);
}
joinParty();
}, [partyId, password]);
const [peers, setPeers] = useState({});
@ -127,7 +134,11 @@ function useSession(
// Setup event listeners for the socket
useEffect(() => {
function addPeer(id, initiator, sync) {
const connection = new Peer({ initiator, trickle: false });
const connection = new Peer({
initiator,
trickle: false,
config: { iceServers },
});
setPeers((prevPeers) => ({
...prevPeers,
@ -178,7 +189,7 @@ function useSession(
socket.removeListener("signal", handleSignal);
socket.removeListener("auth error", handleAuthError);
};
}, [peers, setAuthenticationStatus]);
}, [peers, setAuthenticationStatus, iceServers]);
return { peers, socket };
}

View File

@ -2,15 +2,15 @@ import React, { useRef, useState, useEffect, useContext } from "react";
import { Box, Button, Flex, Label, Text } from "theme-ui";
import shortid from "shortid";
import db from "../database";
import Modal from "../components/Modal";
import MapTiles from "../components/map/MapTiles";
import MapSettings from "../components/map/MapSettings";
import AuthContext from "../contexts/AuthContext";
import DatabaseContext from "../contexts/DatabaseContext";
import usePrevious from "../helpers/usePrevious";
import blobToBuffer from "../helpers/blobToBuffer";
import { maps as defaultMaps } from "../maps";
@ -42,6 +42,7 @@ function SelectMapModal({
// The map currently being view in the map screen
currentMap,
}) {
const { database } = useContext(DatabaseContext);
const { userId } = useContext(AuthContext);
const wasOpen = usePrevious(isOpen);
@ -54,7 +55,7 @@ function SelectMapModal({
const [maps, setMaps] = useState([]);
// Load maps from the database and ensure state is properly setup
useEffect(() => {
if (!userId) {
if (!userId || !database) {
return;
}
async function getDefaultMaps() {
@ -72,16 +73,16 @@ function SelectMapModal({
...defaultMapProps,
});
// Add a state for the map if there isn't one already
const state = await db.table("states").get(id);
const state = await database.table("states").get(id);
if (!state) {
await db.table("states").add({ ...defaultMapState, mapId: id });
await database.table("states").add({ ...defaultMapState, mapId: id });
}
}
return defaultMapsWithIds;
}
async function loadMaps() {
let storedMaps = await db
let storedMaps = await database
.table("maps")
.where({ owner: userId })
.toArray();
@ -92,7 +93,7 @@ function SelectMapModal({
// reload map state as is may have changed while the modal was closed
if (selectedMap) {
const state = await db.table("states").get(selectedMap.id);
const state = await database.table("states").get(selectedMap.id);
if (state) {
setSelectedMapState(state);
}
@ -102,7 +103,7 @@ function SelectMapModal({
if (!wasOpen && isOpen) {
loadMaps();
}
}, [userId, isOpen, wasOpen, selectedMap]);
}, [userId, database, isOpen, wasOpen, selectedMap]);
const fileInputRef = useRef();
@ -141,31 +142,35 @@ function SelectMapModal({
let image = new Image();
setImageLoading(true);
// Copy file to avoid permissions issues
const copy = new Blob([file], { type: file.type });
// Create and load the image temporarily to get its dimensions
const url = URL.createObjectURL(copy);
image.onload = function () {
handleMapAdd({
file: copy,
name,
type: "file",
gridX: fileGridX,
gridY: fileGridY,
width: image.width,
height: image.height,
id: shortid.generate(),
created: Date.now(),
lastModified: Date.now(),
owner: userId,
...defaultMapProps,
});
setImageLoading(false);
URL.revokeObjectURL(url);
};
image.src = url;
// Set file input to null to allow adding the same image 2 times in a row
fileInputRef.current.value = null;
blobToBuffer(file).then((buffer) => {
// Copy file to avoid permissions issues
const blob = new Blob([buffer]);
// Create and load the image temporarily to get its dimensions
const url = URL.createObjectURL(blob);
image.onload = function () {
handleMapAdd({
// Save as a buffer to send with msgpack
file: buffer,
name,
type: "file",
gridX: fileGridX,
gridY: fileGridY,
width: image.width,
height: image.height,
id: shortid.generate(),
created: Date.now(),
lastModified: Date.now(),
owner: userId,
...defaultMapProps,
});
setImageLoading(false);
URL.revokeObjectURL(url);
};
image.src = url;
// Set file input to null to allow adding the same image 2 times in a row
fileInputRef.current.value = null;
});
}
function openImageDialog() {
@ -175,21 +180,21 @@ function SelectMapModal({
}
async function handleMapAdd(map) {
await db.table("maps").add(map);
await database.table("maps").add(map);
const state = { ...defaultMapState, mapId: map.id };
await db.table("states").add(state);
await database.table("states").add(state);
setMaps((prevMaps) => [map, ...prevMaps]);
setSelectedMap(map);
setSelectedMapState(state);
}
async function handleMapRemove(id) {
await db.table("maps").delete(id);
await db.table("states").delete(id);
await database.table("maps").delete(id);
await database.table("states").delete(id);
setMaps((prevMaps) => {
const filtered = prevMaps.filter((map) => map.id !== id);
setSelectedMap(filtered[0]);
db.table("states").get(filtered[0].id).then(setSelectedMapState);
database.table("states").get(filtered[0].id).then(setSelectedMapState);
return filtered;
});
// Removed the map from the map screen if needed
@ -199,14 +204,14 @@ function SelectMapModal({
}
async function handleMapSelect(map) {
const state = await db.table("states").get(map.id);
const state = await database.table("states").get(map.id);
setSelectedMapState(state);
setSelectedMap(map);
}
async function handleMapReset(id) {
const state = { ...defaultMapState, mapId: id };
await db.table("states").put(state);
await database.table("states").put(state);
setSelectedMapState(state);
// Reset the state of the current map if needed
if (currentMap && currentMap.id === selectedMap.id) {
@ -256,7 +261,7 @@ function SelectMapModal({
async function handleMapSettingsChange(key, value) {
const change = { [key]: value, lastModified: Date.now() };
db.table("maps").update(selectedMap.id, change);
database.table("maps").update(selectedMap.id, change);
const newMap = { ...selectedMap, ...change };
setMaps((prevMaps) => {
const newMaps = [...prevMaps];
@ -270,7 +275,7 @@ function SelectMapModal({
}
async function handleMapStateSettingsChange(key, value) {
db.table("states").update(selectedMap.id, { [key]: value });
database.table("states").update(selectedMap.id, { [key]: value });
setSelectedMapState((prevState) => ({ ...prevState, [key]: value }));
}

View File

@ -4,20 +4,20 @@ import { Box, Label, Flex, Button, useColorMode, Checkbox } from "theme-ui";
import Modal from "../components/Modal";
import AuthContext from "../contexts/AuthContext";
import db from "../database";
import DatabaseContext from "../contexts/DatabaseContext";
function SettingsModal({ isOpen, onRequestClose }) {
const { database } = useContext(DatabaseContext);
const { userId } = useContext(AuthContext);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
async function handleEraseAllData() {
await db.delete();
await database.delete();
window.location.reload();
}
async function handleClearCache() {
await db.table("maps").where("owner").notEqual(userId).delete();
await database.table("maps").where("owner").notEqual(userId).delete();
// TODO: With custom tokens look up all tokens that aren't being used in a state
window.location.reload();
}

View File

@ -26,7 +26,7 @@ function FAQ() {
<Text mb={2} variant="heading" as="h1" sx={{ fontSize: 5 }}>
Frequently Asked Questions
</Text>
<Text my={1} variant="heading" as="h2" sx={{ fontSize: 3 }}>
<Text my={1} variant="heading" as="h2" sx={{ fontSize: 3 }} id="radio">
Using Radio (experimental)
</Text>
<Text my={1} variant="heading" as="h3">
@ -61,7 +61,13 @@ function FAQ() {
</ExternalLink>{" "}
on the Mozilla Developer Network.
</Text>
<Text my={1} variant="heading" as="h2" sx={{ fontSize: 3 }}>
<Text
my={1}
variant="heading"
as="h2"
sx={{ fontSize: 3 }}
id="connection"
>
Connection
</Text>
<Text my={1} variant="heading" as="h3">
@ -131,6 +137,20 @@ function FAQ() {
</ExternalLink>
.
</Text>
<Text my={1} variant="heading" as="h2" sx={{ fontSize: 3 }} id="saving">
Saving
</Text>
<Text my={1} variant="heading" as="h3">
Database is disabled.
</Text>
<Text variant="body2" as="p">
Owlbear Rodeo uses a local database to store saved data. If you are
seeing a database is disabled message this usually means you have data
storage disabled. The most common occurances of this is if you are
using Private Browsing modes or in Firefox have the Never Remember
History option enabled. The site will still function in these cases
however all data will be lost when the page closes or reloads.
</Text>
</Flex>
<Footer />
</Flex>

View File

@ -2,8 +2,6 @@ import React, { useState, useEffect, useCallback, useContext } from "react";
import { Flex, Box, Text } from "theme-ui";
import { useParams } from "react-router-dom";
import db from "../database";
import { omit, isStreamStopped } from "../helpers/shared";
import useSession from "../helpers/useSession";
import useDebounce from "../helpers/useDebounce";
@ -18,10 +16,12 @@ import Link from "../components/Link";
import AuthModal from "../modals/AuthModal";
import AuthContext from "../contexts/AuthContext";
import DatabaseContext from "../contexts/DatabaseContext";
import { tokens as defaultTokens } from "../tokens";
function Game() {
const { database } = useContext(DatabaseContext);
const { id: gameId } = useParams();
const { authenticationStatus, userId, nickname, setNickname } = useContext(
AuthContext
@ -78,11 +78,14 @@ function Game() {
debouncedMapState &&
debouncedMapState.mapId &&
map &&
map.owner === userId
map.owner === userId &&
database
) {
db.table("states").update(debouncedMapState.mapId, debouncedMapState);
database
.table("states")
.update(debouncedMapState.mapId, debouncedMapState);
}
}, [map, debouncedMapState, userId]);
}, [map, debouncedMapState, userId, database]);
function handleMapChange(newMap, newMapState) {
setMapState(newMapState);
@ -267,7 +270,8 @@ function Game() {
const newMap = data.data;
// If is a file map check cache and request the full file if outdated
if (newMap && newMap.type === "file") {
db.table("maps")
database
.table("maps")
.get(newMap.id)
.then((cachedMap) => {
if (cachedMap && cachedMap.lastModified === newMap.lastModified) {
@ -289,11 +293,10 @@ function Game() {
if (data.id === "mapResponse") {
setMapLoading(false);
if (data.data && data.data.type === "file") {
// Convert file back to blob after peer transfer
const file = new Blob([data.data.file]);
const newMap = { ...data.data, file };
const newMap = { ...data.data, file: data.data.file };
// Store in db
db.table("maps")
database
.table("maps")
.put(newMap)
.then(() => {
setMap(newMap);
@ -498,7 +501,8 @@ function Game() {
<Banner isOpen={!!peerError} onRequestClose={() => setPeerError(null)}>
<Box p={1}>
<Text as="p" variant="body2">
{peerError} See <Link to="/faq">FAQ</Link> for more information.
{peerError} See <Link to="/faq#connection">FAQ</Link> for more
information.
</Text>
</Box>
</Banner>

View File

@ -1398,6 +1398,11 @@
call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0"
"@msgpack/msgpack@^1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-1.12.1.tgz#aab1084bc33c955501bc0f202099e38304143e0b"
integrity sha512-nGwwmkdm3tuLdEkWMIwLBgFBfMFILILxcZIQY0dfqsdboN2iZdKfOYKUOKoa0wXw1FL1PL3yEYGPCXhwodQDTA==
"@nodelib/fs.stat@^1.1.2":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
@ -2647,6 +2652,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
base64-arraybuffer-es6@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.5.0.tgz#27877d01148bcfb3919c17ecf64ea163d9bdba62"
integrity sha512-UCIPaDJrNNj5jG2ZL+nzJ7czvZV/ZYX6LaIRgfVU1k1edJOQg7dkbiSKzwHkNp6aHEHER/PhlFBrMYnlvJJQEw==
base64-arraybuffer@0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
@ -3488,7 +3498,7 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
core-js@^2.4.0:
core-js@^2.4.0, core-js@^2.5.3:
version "2.6.11"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
@ -4746,6 +4756,14 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
fake-indexeddb@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-3.0.0.tgz#1bd0ffce41b0f433409df301d334d8fd7d77da27"
integrity sha512-VrnV9dJWlVWvd8hp9MMR+JS4RLC4ZmToSkuCg91ZwpYE5mSODb3n5VEaV62Hf3AusnbrPfwQhukU+rGZm5W8PQ==
dependencies:
realistic-structured-clone "^2.0.1"
setimmediate "^1.0.5"
fast-deep-equal@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
@ -6537,11 +6555,6 @@ jest@24.9.0:
import-local "^2.0.0"
jest-cli "^24.9.0"
js-binarypack@^0.0.9:
version "0.0.9"
resolved "https://registry.yarnpkg.com/js-binarypack/-/js-binarypack-0.0.9.tgz#454243d3de212961cc1514a2f119dec2faf64035"
integrity sha1-RUJD094hKWHMFRSi8Rnewvr2QDU=
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -8830,7 +8843,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.3"
prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -9089,6 +9102,13 @@ react-router-dom@^5.1.2:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router-hash-link@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/react-router-hash-link/-/react-router-hash-link-1.2.2.tgz#7a0ad5e925d49596d19554de8bc6c554ce4f8099"
integrity sha512-LBthLVHdqPeKDVt3+cFRhy15Z7veikOvdKRZRfyBR2vjqIE7rxn+tKLjb6DOmLm6JpoQVemVDnxQ35RVnEHdQA==
dependencies:
prop-types "^15.6.0"
react-router@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418"
@ -9246,6 +9266,16 @@ readdirp@~3.3.0:
dependencies:
picomatch "^2.0.7"
realistic-structured-clone@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/realistic-structured-clone/-/realistic-structured-clone-2.0.2.tgz#2f8ec225b1f9af20efc79ac96a09043704414959"
integrity sha512-5IEvyfuMJ4tjQOuKKTFNvd+H9GSbE87IcendSBannE28PTrbolgaVg5DdEApRKhtze794iXqVUFKV60GLCNKEg==
dependencies:
core-js "^2.5.3"
domexception "^1.0.1"
typeson "^5.8.2"
typeson-registry "^1.0.0-alpha.20"
realpath-native@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c"
@ -9797,7 +9827,7 @@ set-value@^2.0.0, set-value@^2.0.1:
is-plain-object "^2.0.3"
split-string "^3.0.1"
setimmediate@^1.0.4:
setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
@ -10749,6 +10779,20 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typeson-registry@^1.0.0-alpha.20:
version "1.0.0-alpha.35"
resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.35.tgz#b86abfe440e6ee69102eebb0e8c5a916dd182ff9"
integrity sha512-a/NffrpFswBTyU6w2d6vjk62K1TZ45H64af9AfRbn7LXqNEfL+h+gw3OV2IaG+enfwqgLB5WmbkrNBaQuc/97A==
dependencies:
base64-arraybuffer-es6 "0.5.0"
typeson "5.18.2"
whatwg-url "7.1.0"
typeson@5.18.2, typeson@^5.8.2:
version "5.18.2"
resolved "https://registry.yarnpkg.com/typeson/-/typeson-5.18.2.tgz#0d217fc0e11184a66aa7ca0076d9aa7707eb7bc2"
integrity sha512-Vetd+OGX05P4qHyHiSLdHZ5Z5GuQDrHHwSdjkqho9NSCYVSLSfRMjklD/unpHH8tXBR9Z/R05rwJSuMpMFrdsw==
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@ -11175,19 +11219,19 @@ whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
whatwg-url@^6.4.1:
version "6.5.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
whatwg-url@7.1.0, whatwg-url@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
dependencies:
lodash.sortby "^4.7.0"
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
whatwg-url@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
whatwg-url@^6.4.1:
version "6.5.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
dependencies:
lodash.sortby "^4.7.0"
tr46 "^1.0.1"