Added database faker for when indexedb is disabled

Database is now in a context with a status
New FAQ for indexdb errors
This commit is contained in:
Mitchell McCaffrey 2020-05-03 18:22:09 +10:00
parent 05d5c76c86
commit 60059ff447
11 changed files with 264 additions and 129 deletions

View File

@ -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",

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,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">FAQ</Link> for more
information.
</Text>
</Box>
)}
</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,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 (
<DatabaseContext.Provider value={value}>
{children}
</DatabaseContext.Provider>
);
}
export default DatabaseContext;

View File

@ -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;

View File

@ -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 }));
}

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

@ -131,6 +131,20 @@ function FAQ() {
</ExternalLink>
.
</Text>
<Text my={1} variant="heading" as="h2" sx={{ fontSize: 3 }}>
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) {
@ -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);

View File

@ -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"