Merge branch 'bugfix/map-send' into release/v1.2.1
This commit is contained in:
commit
97d99a3ea1
2
.env
Normal file
2
.env
Normal 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
2
.env.production
Normal file
@ -0,0 +1,2 @@
|
||||
REACT_APP_BROKER_URL=https://agent.owlbear.rodeo
|
||||
REACT_APP_ICE_SERVERS_URL=https://agent.owlbear.rodeo/iceservers
|
@ -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",
|
||||
|
39
src/App.js
39
src/App.js
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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,
|
||||
|
44
src/contexts/DatabaseContext.js
Normal file
44
src/contexts/DatabaseContext.js
Normal 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;
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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 }));
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
76
yarn.lock
76
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user