From 60059ff44797864758616ee7b19cd79ae59b586a Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sun, 3 May 2020 18:22:09 +1000 Subject: [PATCH] Added database faker for when indexedb is disabled Database is now in a context with a status New FAQ for indexdb errors --- package.json | 1 + src/App.js | 39 +++++----- src/components/map/MapTiles.js | 124 +++++++++++++++++++------------- src/contexts/AuthContext.js | 32 +++++---- src/contexts/DatabaseContext.js | 54 ++++++++++++++ src/database.js | 11 --- src/modals/SelectMapModal.js | 34 ++++----- src/modals/SettingsModal.js | 8 +-- src/routes/FAQ.js | 14 ++++ src/routes/Game.js | 19 +++-- yarn.lock | 57 ++++++++++++--- 11 files changed, 264 insertions(+), 129 deletions(-) create mode 100644 src/contexts/DatabaseContext.js delete mode 100644 src/database.js 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. +