diff --git a/package.json b/package.json
index c408468..aefedb6 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"@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",
"normalize-wheel": "^1.0.1",
"react": "^16.13.0",
diff --git a/src/App.js b/src/App.js
index 09fc5e3..470a417 100644
--- a/src/App.js
+++ b/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 (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/components/map/MapTiles.js b/src/components/map/MapTiles.js
index 5babba3..a71e761 100644
--- a/src/components/map/MapTiles.js
+++ b/src/components/map/MapTiles.js
@@ -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 (
-
-
+
+
-
+
+
+
+ {maps.map((map) => (
+
+ ))}
- {maps.map((map) => (
-
- ))}
-
-
+
+ {databaseStatus === "disabled" && (
+
+
+ Map saving is unavailable. See FAQ for more
+ information.
+
+
+ )}
+
);
}
diff --git a/src/contexts/AuthContext.js b/src/contexts/AuthContext.js
index 0d71b2c..e9aced1 100644
--- a/src/contexts/AuthContext.js
+++ b/src/contexts/AuthContext.js
@@ -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,
diff --git a/src/contexts/DatabaseContext.js b/src/contexts/DatabaseContext.js
new file mode 100644
index 0000000..7bbac74
--- /dev/null
+++ b/src/contexts/DatabaseContext.js
@@ -0,0 +1,54 @@
+import React, { useState, useEffect } from "react";
+import Dexie from "dexie";
+
+const DatabaseContext = React.createContext();
+
+export function DatabaseProvider({ children }) {
+ const [database, setDatabase] = useState();
+ const [databaseStatus, setDatabaseStatus] = useState("loading");
+
+ function loadVersions(db) {
+ db.version(1).stores({
+ maps: "id, owner",
+ states: "mapId",
+ tokens: "id, owner",
+ user: "key",
+ });
+ }
+
+ 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 = new Dexie("OwlbearRodeoDB");
+ loadVersions(db);
+ 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 = new Dexie("OwlbearRodeoDB", { indexedDB, IDBKeyRange });
+ loadVersions(db);
+ setDatabase(db);
+ setDatabaseStatus("disabled");
+ window.indexedDB.deleteDatabase("__test");
+ };
+ }, []);
+
+ const value = {
+ database,
+ databaseStatus,
+ };
+ return (
+
+ {children}
+
+ );
+}
+
+export default DatabaseContext;
diff --git a/src/database.js b/src/database.js
deleted file mode 100644
index c1ad236..0000000
--- a/src/database.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import Dexie from "dexie";
-
-const db = new Dexie("OwlbearRodeoDB");
-db.version(1).stores({
- maps: "id, owner",
- states: "mapId",
- tokens: "id, owner",
- user: "key",
-});
-
-export default db;
diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js
index 3ffa96b..cb0cbcb 100644
--- a/src/modals/SelectMapModal.js
+++ b/src/modals/SelectMapModal.js
@@ -2,13 +2,12 @@ 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";
@@ -43,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);
@@ -55,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() {
@@ -73,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();
@@ -93,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);
}
@@ -103,7 +103,7 @@ function SelectMapModal({
if (!wasOpen && isOpen) {
loadMaps();
}
- }, [userId, isOpen, wasOpen, selectedMap]);
+ }, [userId, database, isOpen, wasOpen, selectedMap]);
const fileInputRef = useRef();
@@ -180,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
@@ -204,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) {
@@ -261,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];
@@ -275,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 }));
}
diff --git a/src/modals/SettingsModal.js b/src/modals/SettingsModal.js
index c60cb20..43fbe55 100644
--- a/src/modals/SettingsModal.js
+++ b/src/modals/SettingsModal.js
@@ -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();
}
diff --git a/src/routes/FAQ.js b/src/routes/FAQ.js
index 03a548a..8cb6e3e 100644
--- a/src/routes/FAQ.js
+++ b/src/routes/FAQ.js
@@ -131,6 +131,20 @@ function FAQ() {
.
+
+ Saving
+
+
+ Database is disabled.
+
+
+ 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.
+
diff --git a/src/routes/Game.js b/src/routes/Game.js
index 8243889..47aa799 100644
--- a/src/routes/Game.js
+++ b/src/routes/Game.js
@@ -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) {
@@ -291,7 +295,8 @@ function Game() {
if (data.data && data.data.type === "file") {
const newMap = { ...data.data, file: data.data.file };
// Store in db
- db.table("maps")
+ database
+ .table("maps")
.put(newMap)
.then(() => {
setMap(newMap);
diff --git a/yarn.lock b/yarn.lock
index 4996633..ef5eb89 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2652,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"
@@ -3493,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==
@@ -4751,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"
@@ -9246,6 +9259,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 +9820,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 +10772,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 +11212,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"