Merge pull request #44 from mitchemmc/patch/1.9.0-1

Patch/1.9.0 1
This commit is contained in:
Mitchell McCaffrey 2021-06-25 21:07:27 +10:00 committed by GitHub
commit 9e8898f769
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 306 additions and 347 deletions

View File

@ -21,8 +21,8 @@
"color": "^3.1.3",
"comlink": "^4.3.0",
"deep-diff": "^1.0.2",
"dexie": "^3.0.3",
"dexie-observable": "^3.0.0-beta.10",
"dexie": "3.1.0-beta.13",
"dexie-react-hooks": "^1.0.6",
"err-code": "^3.0.1",
"fake-indexeddb": "^3.1.2",
"file-saver": "^2.0.5",
@ -30,6 +30,7 @@
"image-outline": "^0.1.0",
"intersection-observer": "^0.12.0",
"konva": "^7.2.5",
"lodash.chunk": "^4.2.0",
"lodash.clonedeep": "^4.5.0",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",

View File

@ -1,6 +1,7 @@
import React, { useState, useContext, useCallback, useEffect } from "react";
import * as Comlink from "comlink";
import { encode } from "@msgpack/msgpack";
import { useLiveQuery } from "dexie-react-hooks";
import { useDatabase } from "./DatabaseContext";
@ -128,6 +129,47 @@ export const AssetURLsUpdaterContext = React.createContext();
*/
export function AssetURLsProvider({ children }) {
const [assetURLs, setAssetURLs] = useState({});
const { database } = useDatabase();
// Keep track of when the asset keys change so we can update the URLs
const [assetKeys, setAssetKeys] = useState([]);
useEffect(() => {
const keys = Object.keys(assetURLs);
let newKeys = keys.filter((key) => !assetKeys.includes(key));
let deletedKeys = assetKeys.filter((key) => !keys.includes(key));
if (newKeys.length > 0 || deletedKeys.length > 0) {
setAssetKeys((prevKeys) =>
[...prevKeys, ...newKeys].filter((key) => !deletedKeys.includes(key))
);
}
}, [assetURLs, assetKeys]);
// Get the new assets whenever the keys change
const assets = useLiveQuery(
() => database?.table("assets").where("id").anyOf(assetKeys).toArray(),
[database, assetKeys]
);
// Update asset URLs when assets are loaded
useEffect(() => {
if (!assets) {
return;
}
setAssetURLs((prevURLs) => {
let newURLs = { ...prevURLs };
for (let asset of assets) {
if (newURLs[asset.id].url === null) {
newURLs[asset.id] = {
...newURLs[asset.id],
url: URL.createObjectURL(
new Blob([asset.file], { type: asset.mime })
),
};
}
}
return newURLs;
});
}, [assets]);
// Clean up asset URLs every minute
const debouncedAssetURLs = useDebounce(assetURLs, 60 * 1000);
@ -177,16 +219,8 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource) {
throw new Error("useAssetURL must be used within a AssetURLsProvider");
}
const { getAsset } = useAssets();
const { database, databaseStatus } = useDatabase();
useEffect(() => {
if (
!assetId ||
type !== "file" ||
!database ||
databaseStatus === "loading"
) {
if (!assetId || type !== "file") {
return;
}
@ -201,13 +235,10 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource) {
};
}
function createURL(prevURLs, asset) {
const url = URL.createObjectURL(
new Blob([asset.file], { type: asset.mime })
);
function createReference(prevURLs) {
return {
...prevURLs,
[assetId]: { url, id: assetId, references: 1 },
[assetId]: { url: null, id: assetId, references: 1 },
};
}
setAssetURLs((prevURLs) => {
@ -215,59 +246,14 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource) {
// Check if the asset url is already added and increase references
return increaseReferences(prevURLs);
} else {
getAsset(assetId).then((asset) => {
if (!asset) {
return;
}
setAssetURLs((prevURLs) => {
if (assetId in prevURLs) {
// Check again if it exists
return increaseReferences(prevURLs);
} else {
// Create url if the asset doesn't have a url
return createURL(prevURLs, asset);
}
});
});
return prevURLs;
return createReference(prevURLs);
}
});
}
updateAssetURL();
// Update the url when the asset is added to the db after the hook is used
function handleAssetChanges(changes) {
for (let change of changes) {
const id = change.key;
if (
change.table === "assets" &&
id === assetId &&
(change.type === 1 || change.type === 2)
) {
const asset = change.obj;
setAssetURLs((prevURLs) => {
if (!(assetId in prevURLs)) {
const url = URL.createObjectURL(
new Blob([asset.file], { type: asset.mime })
);
return {
...prevURLs,
[assetId]: { url, id: assetId, references: 1 },
};
} else {
return prevURLs;
}
});
}
}
}
database.on("changes", handleAssetChanges);
return () => {
database.on("changes").unsubscribe(handleAssetChanges);
// Decrease references
setAssetURLs((prevURLs) => {
if (assetId in prevURLs) {
@ -283,7 +269,7 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource) {
}
});
};
}, [assetId, setAssetURLs, getAsset, type, database, databaseStatus]);
}, [assetId, setAssetURLs, type]);
if (!assetId) {
return unknownSource;

View File

@ -40,7 +40,7 @@ export function DatabaseProvider({ children }) {
undefined,
undefined,
true,
(v) => {
() => {
setDatabaseStatus("upgrading");
}
);

View File

@ -1,9 +1,14 @@
import React, { useEffect, useState, useContext, useCallback } from "react";
import React, {
useEffect,
useState,
useContext,
useCallback,
useMemo,
} from "react";
import { useLiveQuery } from "dexie-react-hooks";
import { useUserId } from "./UserIdContext";
import { useDatabase } from "./DatabaseContext";
import { applyObservableChange } from "../helpers/dexie";
import { removeGroupsItems } from "../helpers/group";
const MapDataContext = React.createContext();
@ -18,33 +23,39 @@ const defaultMapState = {
};
export function MapDataProvider({ children }) {
const { database, databaseStatus } = useDatabase();
const userId = useUserId();
const { database } = useDatabase();
const mapsQuery = useLiveQuery(
() => database?.table("maps").toArray(),
[database]
);
const mapStatesQuery = useLiveQuery(
() => database?.table("states").toArray(),
[database]
);
const maps = useMemo(() => mapsQuery || [], [mapsQuery]);
const mapStates = useMemo(() => mapStatesQuery || [], [mapStatesQuery]);
const mapsLoading = useMemo(
() => !mapsQuery || !mapStatesQuery,
[mapsQuery, mapStatesQuery]
);
const mapGroupQuery = useLiveQuery(
() => database?.table("groups").get("maps"),
[database]
);
const [maps, setMaps] = useState([]);
const [mapStates, setMapStates] = useState([]);
const [mapsLoading, setMapsLoading] = useState(true);
const [mapGroups, setMapGroups] = useState([]);
// Load maps from the database and ensure state is properly setup
useEffect(() => {
if (!userId || !database || databaseStatus === "loading") {
return;
}
async function loadMaps() {
const storedMaps = await database.table("maps").toArray();
setMaps(storedMaps);
const storedStates = await database.table("states").toArray();
setMapStates(storedStates);
async function updateMapGroups() {
const group = await database.table("groups").get("maps");
const storedGroups = group.items;
setMapGroups(storedGroups);
setMapsLoading(false);
setMapGroups(group.items);
}
loadMaps();
}, [userId, database, databaseStatus]);
if (database && mapGroupQuery) {
updateMapGroups();
}
}, [mapGroupQuery, database]);
const getMap = useCallback(
async (mapId) => {
@ -138,77 +149,6 @@ export function MapDataProvider({ children }) {
[database]
);
// Create DB observable to sync creating and deleting
useEffect(() => {
if (!database || databaseStatus === "loading") {
return;
}
function handleMapChanges(changes) {
for (let change of changes) {
if (change.table === "maps") {
if (change.type === 1) {
// Created
const map = change.obj;
const state = { ...defaultMapState, mapId: map.id };
setMaps((prevMaps) => [map, ...prevMaps]);
setMapStates((prevStates) => [state, ...prevStates]);
} else if (change.type === 2) {
const map = change.obj;
setMaps((prevMaps) => {
const newMaps = [...prevMaps];
const i = newMaps.findIndex((m) => m.id === map.id);
if (i > -1) {
newMaps[i] = map;
}
return newMaps;
});
} else if (change.type === 3) {
// Deleted
const id = change.key;
setMaps((prevMaps) => {
const filtered = prevMaps.filter((map) => map.id !== id);
return filtered;
});
setMapStates((prevMapsStates) => {
const filtered = prevMapsStates.filter(
(state) => state.mapId !== id
);
return filtered;
});
}
}
if (change.table === "states") {
if (change.type === 2) {
// Update map state
const state = change.obj;
setMapStates((prevMapStates) => {
const newStates = [...prevMapStates];
const i = newStates.findIndex((s) => s.mapId === state.mapId);
if (i > -1) {
newStates[i] = state;
}
return newStates;
});
}
}
if (change.table === "groups") {
if (change.type === 2 && change.key === "maps") {
const group = applyObservableChange(change);
const groups = group.items.filter((item) => item !== null);
setMapGroups(groups);
}
}
}
}
database.on("changes", handleMapChanges);
return () => {
database.on("changes").unsubscribe(handleMapChanges);
};
}, [database, databaseStatus]);
const [mapsById, setMapsById] = useState({});
useEffect(() => {
setMapsById(

View File

@ -1,37 +1,44 @@
import React, { useEffect, useState, useContext, useCallback } from "react";
import React, {
useEffect,
useState,
useContext,
useCallback,
useMemo,
} from "react";
import { useLiveQuery } from "dexie-react-hooks";
import { useUserId } from "./UserIdContext";
import { useDatabase } from "./DatabaseContext";
import { applyObservableChange } from "../helpers/dexie";
import { removeGroupsItems } from "../helpers/group";
const TokenDataContext = React.createContext();
export function TokenDataProvider({ children }) {
const { database, databaseStatus } = useDatabase();
const userId = useUserId();
const { database } = useDatabase();
const tokensQuery = useLiveQuery(
() => database?.table("tokens").toArray(),
[database]
);
const tokens = useMemo(() => tokensQuery || [], [tokensQuery]);
const tokensLoading = useMemo(() => !tokensQuery, [tokensQuery]);
const tokenGroupQuery = useLiveQuery(
() => database?.table("groups").get("tokens"),
[database]
);
const [tokens, setTokens] = useState([]);
const [tokensLoading, setTokensLoading] = useState(true);
const [tokenGroups, setTokenGroups] = useState([]);
useEffect(() => {
if (!userId || !database || databaseStatus === "loading") {
return;
}
async function loadTokens() {
const storedTokens = await database.table("tokens").toArray();
setTokens(storedTokens);
async function updateTokenGroups() {
const group = await database.table("groups").get("tokens");
const storedGroups = group.items;
setTokenGroups(storedGroups);
setTokensLoading(false);
setTokenGroups(group.items);
}
loadTokens();
}, [userId, database, databaseStatus]);
if (database && tokenGroupQuery) {
updateTokenGroups();
}
}, [tokenGroupQuery, database]);
const getToken = useCallback(
async (tokenId) => {
@ -83,15 +90,15 @@ export function TokenDataProvider({ children }) {
const updateTokensHidden = useCallback(
async (ids, hideInSidebar) => {
// Update immediately to avoid UI delay
setTokens((prevTokens) => {
let newTokens = [...prevTokens];
for (let id of ids) {
const tokenIndex = newTokens.findIndex((token) => token.id === id);
newTokens[tokenIndex].hideInSidebar = hideInSidebar;
}
return newTokens;
});
// // Update immediately to avoid UI delay
// setTokens((prevTokens) => {
// let newTokens = [...prevTokens];
// for (let id of ids) {
// const tokenIndex = newTokens.findIndex((token) => token.id === id);
// newTokens[tokenIndex].hideInSidebar = hideInSidebar;
// }
// return newTokens;
// });
await Promise.all(
ids.map((id) => database.table("tokens").update(id, { hideInSidebar }))
);
@ -108,67 +115,6 @@ export function TokenDataProvider({ children }) {
[database]
);
// Create DB observable to sync creating and deleting
useEffect(() => {
if (!database || databaseStatus === "loading") {
return;
}
function handleTokenChanges(changes) {
// Pool token changes together to call a single state update at the end
let tokensCreated = [];
let tokensUpdated = {};
let tokensDeleted = [];
for (let change of changes) {
if (change.table === "tokens") {
if (change.type === 1) {
// Created
const token = change.obj;
tokensCreated.push(token);
} else if (change.type === 2) {
// Updated
const token = change.obj;
tokensUpdated[token.id] = token;
} else if (change.type === 3) {
// Deleted
const id = change.key;
tokensDeleted.push(id);
}
}
if (change.table === "groups") {
if (change.type === 2 && change.key === "tokens") {
const group = applyObservableChange(change);
const groups = group.items.filter((item) => item !== null);
setTokenGroups(groups);
}
}
}
const tokensUpdatedArray = Object.values(tokensUpdated);
if (
tokensCreated.length > 0 ||
tokensUpdatedArray.length > 0 ||
tokensDeleted.length > 0
) {
setTokens((prevTokens) => {
let newTokens = [...tokensCreated, ...prevTokens];
for (let token of tokensUpdatedArray) {
const tokenIndex = newTokens.findIndex((t) => t.id === token.id);
if (tokenIndex > -1) {
newTokens[tokenIndex] = token;
}
}
return newTokens.filter((token) => !tokensDeleted.includes(token.id));
});
}
}
database.on("changes", handleTokenChanges);
return () => {
database.on("changes").unsubscribe(handleTokenChanges);
};
}, [database, databaseStatus]);
const [tokensById, setTokensById] = useState({});
useEffect(() => {
setTokensById(

View File

@ -1,7 +1,6 @@
// eslint-disable-next-line no-unused-vars
import Dexie, { DexieOptions } from "dexie";
import { v4 as uuid } from "uuid";
import "dexie-observable";
import { loadVersions } from "./upgrade";
import { getDefaultMaps } from "./maps";

View File

@ -75,6 +75,9 @@ export function groupBy(array, key) {
}
export const isMacLike = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
export const isSafari = /^((?!chrome|android).)*safari/i.test(
navigator.userAgent
);
export function shuffle(array) {
let temp = [...array];

View File

@ -81,6 +81,7 @@ function ImportExportModal({ isOpen, onRequestClose }) {
)
);
} else {
console.error(e);
setError(e);
}
}
@ -233,7 +234,7 @@ function ImportExportModal({ isOpen, onRequestClose }) {
.bulkGet(Object.keys(newAssetIds));
let assets = [];
for (let asset of assetsToAdd) {
assets.push({ ...asset, id: newAssetIds[asset.id] });
assets.push({ ...asset, id: newAssetIds[asset.id], owner: userId });
}
await db.table("assets").bulkAdd(assets);

View File

@ -3,6 +3,7 @@ import Dexie, { Version } from "dexie";
import shortid from "shortid";
import { v4 as uuid } from "uuid";
import Case from "case";
import chunk from "lodash.chunk";
import blobToBuffer from "./helpers/blobToBuffer";
import { getGridDefaultInset } from "./helpers/grid";
@ -475,10 +476,19 @@ export const versions = {
},
// v1.9.0 - Move map assets into new table
24(v, onUpgrade) {
v.stores({ assets: "id, owner" }).upgrade((tx) => {
v.stores({ assets: "id, owner" }).upgrade(async (tx) => {
onUpgrade?.(24);
tx.table("maps").each((map) => {
const primaryKeys = await Dexie.waitFor(
tx.table("maps").toCollection().primaryKeys()
);
const keyChunks = chunk(primaryKeys, 4);
for (let keys of keyChunks) {
let assets = [];
let maps = await Dexie.waitFor(tx.table("maps").bulkGet(keys));
while (maps.length > 0) {
const map = maps.pop();
assets.push({
id: uuid(),
owner: map.owner,
@ -515,17 +525,28 @@ export const versions = {
prevId: map.id,
prevType: "mapThumbnail",
});
tx.table("assets").bulkAdd(assets);
});
}
maps = null;
await tx.table("assets").bulkAdd(assets);
assets = null;
}
});
},
// v1.9.0 - Move token assets into new table
25(v, onUpgrade) {
v.stores({}).upgrade((tx) => {
v.stores({}).upgrade(async (tx) => {
onUpgrade?.(25);
tx.table("tokens").each((token) => {
const primaryKeys = await Dexie.waitFor(
tx.table("tokens").toCollection().primaryKeys()
);
const keyChunks = chunk(primaryKeys, 4);
for (let keys of keyChunks) {
let assets = [];
let tokens = await Dexie.waitFor(tx.table("tokens").bulkGet(keys));
while (tokens.length > 0) {
let token = tokens.pop();
assets.push({
id: uuid(),
owner: token.owner,
@ -546,32 +567,75 @@ export const versions = {
prevId: token.id,
prevType: "tokenThumbnail",
});
tx.table("assets").bulkAdd(assets);
});
}
tokens = null;
await tx.table("assets").bulkAdd(assets);
assets = null;
}
});
},
// v1.9.0 - Create foreign keys for assets
26(v, onUpgrade) {
v.stores({}).upgrade((tx) => {
v.stores({}).upgrade(async (tx) => {
onUpgrade?.(26);
tx.table("assets").each((asset) => {
if (asset.prevType === "map") {
tx.table("maps").update(asset.prevId, {
file: asset.id,
});
} else if (asset.prevType === "token") {
tx.table("tokens").update(asset.prevId, {
file: asset.id,
});
} else if (asset.prevType === "mapThumbnail") {
tx.table("maps").update(asset.prevId, { thumbnail: asset.id });
} else if (asset.prevType === "tokenThumbnail") {
tx.table("tokens").update(asset.prevId, { thumbnail: asset.id });
} else if (asset.prevType === "mapResolution") {
tx.table("maps").update(asset.prevId, {
resolutions: undefined,
[asset.resolution]: asset.id,
let mapUpdates = {};
let tokenUpdates = {};
const primaryKeys = await Dexie.waitFor(
tx.table("assets").toCollection().primaryKeys()
);
const keyChunks = chunk(primaryKeys, 4);
for (let keys of keyChunks) {
let assets = await Dexie.waitFor(tx.table("assets").bulkGet(keys));
while (assets.length > 0) {
const asset = assets.pop();
const { prevId, id, prevType, resolution } = asset;
if (prevType === "token" || prevType === "tokenThumbnail") {
if (!(prevId in tokenUpdates)) {
tokenUpdates[prevId] = {};
}
} else {
if (!(prevId in mapUpdates)) {
mapUpdates[prevId] = {};
}
}
if (prevType === "map") {
mapUpdates[prevId].file = id;
} else if (prevType === "token") {
tokenUpdates[prevId].file = id;
} else if (prevType === "mapThumbnail") {
mapUpdates[prevId].thumbnail = id;
} else if (prevType === "tokenThumbnail") {
tokenUpdates[prevId].thumbnail = id;
} else if (prevType === "mapResolution") {
mapUpdates[prevId][resolution] = id;
}
}
assets = null;
}
await tx
.table("maps")
.toCollection()
.modify((map) => {
if (map.id in mapUpdates) {
for (let key in mapUpdates[map.id]) {
map[key] = mapUpdates[map.id][key];
}
}
delete map.resolutions;
});
await tx
.table("tokens")
.toCollection()
.modify((token) => {
if (token.id in tokenUpdates) {
for (let key in tokenUpdates[token.id]) {
token[key] = tokenUpdates[token.id][key];
}
}
});
});
@ -580,14 +644,10 @@ export const versions = {
27(v, onUpgrade) {
v.stores({}).upgrade((tx) => {
onUpgrade?.(27);
tx.table("assets")
.toCollection()
.modify((asset) => {
delete asset.prevId;
if (asset.prevType === "mapResolution") {
delete asset.resolution;
}
delete asset.prevType;
tx.table("assets").toCollection().modify({
prevId: undefined,
prevType: undefined,
resolution: undefined,
});
});
},
@ -771,9 +831,17 @@ export const versions = {
});
});
},
36(v) {
v.stores({
_changes: null,
_intercomm: null,
_syncNodes: null,
_uncommittedChanges: null,
});
},
};
export const latestVersion = 35;
export const latestVersion = 36;
/**
* Load versions onto a database up to a specific version number

View File

@ -8,6 +8,7 @@ import { encode, decode } from "@msgpack/msgpack";
import { getDatabase } from "../database";
import blobToBuffer from "../helpers/blobToBuffer";
import { isSafari } from "../helpers/shared";
// Worker to load large amounts of database data on a separate thread
let service = {
@ -45,6 +46,10 @@ let service = {
* @param {string} table
*/
async putData(data, table) {
if (isSafari) {
// Safari is unable to put data into indexedb and have useLiveQuery update
return false;
}
try {
let db = getDatabase({});
const decoded = decode(data);

View File

@ -5369,12 +5369,17 @@ detect-port-alt@1.1.6:
address "^1.0.1"
debug "^2.6.0"
dexie-observable@^3.0.0-beta.10:
version "3.0.0-beta.10"
resolved "https://registry.yarnpkg.com/dexie-observable/-/dexie-observable-3.0.0-beta.10.tgz#ad7a7e136defbb62f9eab9198a5cb9e10bce1c87"
integrity sha512-GMPwQMLh1nYqM1MYsOZudsIwSMqDMrAOBxNuw+Y2ijsrQTBPi3nRF2CinY02IdlmffkaU7DsDfnlgdaMEaiHTQ==
dexie-react-hooks@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/dexie-react-hooks/-/dexie-react-hooks-1.0.6.tgz#a21d116addb2bb785507bf924cc6e963d858d5d5"
integrity sha512-OFoOBC4BQzkVGicuWl/cIMtlPp0wTAnUXwUJzq+l/zp0XVGmwEWkemRFq7JbudJLT0DINFVVzgVhGV7KOUK7uA==
"dexie@^3.0.0-alpha.5 || ^2.0.4", dexie@^3.0.3:
dexie@3.1.0-beta.13:
version "3.1.0-beta.13"
resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.1.0-beta.13.tgz#54b3438e2aca3b60f87a823a535ce1b4313056ec"
integrity sha512-pUcX9YyX1VDjF1oMqiOys6N2zoXIA/CeTghB3P4Ee77U8n9q0qa2pmNYoHyyYPKLU58+gzsMJuOc6HLPJDQrQQ==
"dexie@^3.0.0-alpha.5 || ^2.0.4":
version "3.0.3"
resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.0.3.tgz#ede63849dfe5f07e13e99bb72a040e8ac1d29dab"
integrity sha512-BSFhGpngnCl1DOr+8YNwBDobRMH0ziJs2vts69VilwetHYOtEDcLqo7d/XiIphM0tJZ2rPPyAGd31lgH2Ln3nw==
@ -8599,6 +8604,11 @@ lodash.camelcase@^4.3.0:
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
lodash.chunk@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc"
integrity sha1-ZuXOH3btJ7QwPYxlEujRIW6BBrw=
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"