Merge branch 'master' into typescript

This commit is contained in:
Mitchell McCaffrey
2021-07-02 15:54:54 +10:00
157 changed files with 8114 additions and 4055 deletions

View File

@@ -3,96 +3,59 @@ import React, {
useState,
useContext,
useCallback,
useRef,
useMemo,
} from "react";
import { decode } from "@msgpack/msgpack";
import { useLiveQuery } from "dexie-react-hooks";
import { useAuth } from "./AuthContext";
import { useDatabase } from "./DatabaseContext";
import { DefaultToken, FileToken, Token, tokens as defaultTokens } from "../tokens";
import { Token } from "../tokens";
import { removeGroupsItems } from "../helpers/group";
type TokenDataContext = {
tokens: Token[];
ownedTokens: Token[];
addToken: (token: Token) => Promise<void>;
removeToken: (id: string) => Promise<void>;
tokenGroups: any[];
removeTokens: (ids: string[]) => Promise<void>;
updateToken: (id: string, update: Partial<Token>) => Promise<void>;
updateTokens: (ids: string[], update: Partial<Token>) => Promise<void>;
putToken: (token: Token) => Promise<void>;
getToken: (tokenId: string) => Token | undefined
tokensById: { [key: string]: Token; };
getToken: (tokenId: string) => Promise<Token | undefined>;
tokensById: Record<string, Token>;
tokensLoading: boolean;
getTokenFromDB: (tokenId: string) => Promise<Token>;
loadTokens: (tokenIds: string[]) => Promise<void>;
}
updateTokenGroups: (groups: any[]) => void;
updateTokensHidden: (ids: string[], hideInSidebar: boolean) => void;
};
const TokenDataContext = React.createContext<TokenDataContext | undefined>(undefined);
const TokenDataContext =
React.createContext<TokenDataContext | undefined>(undefined);
const cachedTokenMax = 100;
export function TokenDataProvider({ children }: { children: React.ReactNode }) {
const { database } = useDatabase();
export function TokenDataProvider({ children }: { children: any }) {
const { database, databaseStatus, worker } = useDatabase();
const { userId } = useAuth();
const tokensQuery = useLiveQuery<Token[]>(
() => database?.table("tokens").toArray() || [],
[database]
);
/**
* Contains all tokens without any file data,
* to ensure file data is present call loadTokens
*/
const [tokens, setTokens] = useState<Token[]>([]);
const [tokensLoading, setTokensLoading] = useState(true);
const tokens = useMemo(() => tokensQuery || [], [tokensQuery]);
const tokensLoading = useMemo(() => !tokensQuery, [tokensQuery]);
const tokenGroupQuery = useLiveQuery(
() => database?.table("groups").get("tokens"),
[database]
);
const [tokenGroups, setTokenGroups] = useState([]);
useEffect(() => {
if (!userId || !database || databaseStatus === "loading") {
return;
async function updateTokenGroups() {
const group = await database?.table("groups").get("tokens");
setTokenGroups(group.items);
}
function getDefaultTokens() {
const defaultTokensWithIds: Required<DefaultToken[]> = [];
for (let defaultToken of defaultTokens) {
defaultTokensWithIds.push({
...defaultToken,
id: `__default-${defaultToken.name}`,
owner: userId,
});
}
return defaultTokensWithIds;
if (database && tokenGroupQuery) {
updateTokenGroups();
}
}, [tokenGroupQuery, database]);
// Loads tokens without the file data to save memory
async function loadTokens() {
let storedTokens: any = [];
// Try to load tokens with worker, fallback to database if failed
const packedTokens: ArrayLike<number> | BufferSource = await worker.loadData("tokens");
if (packedTokens) {
storedTokens = decode(packedTokens);
} else {
console.warn("Unable to load tokens with worker, loading may be slow");
await database?.table("tokens").each((token: FileToken) => {
const { file, ...rest } = token;
storedTokens.push(rest);
});
}
const sortedTokens = storedTokens.sort((a: any, b: any) => b.created - a.created);
const defaultTokensWithIds = getDefaultTokens();
const allTokens = [...sortedTokens, ...defaultTokensWithIds];
setTokens(allTokens);
setTokensLoading(false);
}
loadTokens();
}, [userId, database, databaseStatus, worker]);
const tokensRef = useRef(tokens);
useEffect(() => {
tokensRef.current = tokens;
}, [tokens]);
const getToken = useCallback((tokenId) => {
return tokensRef.current.find((token) => token.id === tokenId);
}, []);
const getTokenFromDB = useCallback(
const getToken = useCallback(
async (tokenId) => {
let token = await database?.table("tokens").get(tokenId);
return token;
@@ -100,165 +63,89 @@ export function TokenDataProvider({ children }: { children: any }) {
[database]
);
/**
* Keep up to cachedTokenMax amount of tokens that you don't own
* Sorted by when they we're last used
*/
const updateCache = useCallback(async () => {
const cachedTokens: Token[] | undefined = await database?.table("tokens").where("owner").notEqual(userId).sortBy("lastUsed");
// TODO: handle undefined cachedTokens
if (!cachedTokens) {
return;
}
if (cachedTokens?.length > cachedTokenMax) {
const cacheDeleteCount = cachedTokens.length - cachedTokenMax
const idsToDelete = cachedTokens
.slice(0, cacheDeleteCount)
.map((token) => token.id);
database?.table("tokens").where("id").anyOf(idsToDelete).delete();
}
}, [database, userId]);
// Add token and add it to the token group
const addToken = useCallback(
async (token) => {
await database?.table("tokens").add(token);
if (token.owner !== userId) {
await updateCache();
if (database) {
await database.table("tokens").add(token);
const group = await database.table("groups").get("tokens");
await database.table("groups").update("tokens", {
items: [{ id: token.id, type: "item" }, ...group.items],
});
}
},
[database, updateCache, userId]
);
const removeToken = useCallback(
async (id: string) => {
await database?.table("tokens").delete(id);
},
[database]
);
const removeTokens = useCallback(
async (ids: string[]) => {
await database?.table("tokens").bulkDelete(ids);
async (ids) => {
if (database) {
const tokens = await database.table("tokens").bulkGet(ids);
let assetIds = [];
for (let token of tokens) {
if (token.type === "file") {
assetIds.push(token.file);
assetIds.push(token.thumbnail);
}
}
const group = await database.table("groups").get("tokens");
let items = removeGroupsItems(group.items, ids);
await database.table("groups").update("tokens", { items });
await database.table("tokens").bulkDelete(ids);
await database.table("assets").bulkDelete(assetIds);
}
},
[database]
);
const updateToken = useCallback(
async (id: string, update: any) => {
const change = { lastModified: Date.now(), ...update };
await database?.table("tokens").update(id, change);
async (id, update) => {
await database?.table("tokens").update(id, update);
},
[database]
);
const updateTokens = useCallback(
async (ids, update) => {
const change = { lastModified: Date.now(), ...update };
const updateTokensHidden = useCallback(
async (ids: string[], hideInSidebar: boolean) => {
await Promise.all(
ids.map((id: string) => database?.table("tokens").update(id, change))
ids.map((id) => database?.table("tokens").update(id, { hideInSidebar }))
);
},
[database]
);
const putToken = useCallback(
async (token) => {
await database?.table("tokens").put(token);
if (token.owner !== userId) {
await updateCache();
}
},
[database, updateCache, userId]
);
const loadTokens = useCallback(
async (tokenIds: string[]) => {
const loadedTokens: FileToken[] | undefined = await database?.table("tokens").bulkGet(tokenIds);
const loadedTokensById = loadedTokens?.reduce((obj: { [key: string]: FileToken }, token: FileToken) => {
obj[token.id] = token;
return obj;
}, {});
if (!loadedTokensById) {
// TODO: whatever
return;
}
setTokens((prevTokens: Token[]) => {
return prevTokens.map((prevToken) => {
if (prevToken.id in loadedTokensById) {
return loadedTokensById[prevToken.id];
} else {
return prevToken;
}
});
});
const updateTokenGroups = useCallback(
async (groups) => {
// Update group state immediately to avoid animation delay
setTokenGroups(groups);
await database?.table("groups").update("tokens", { items: groups });
},
[database]
);
// Create DB observable to sync creating and deleting
const [tokensById, setTokensById] = useState({});
useEffect(() => {
if (!database || databaseStatus === "loading") {
return;
}
function handleTokenChanges(changes: any) {
for (let change of changes) {
if (change.table === "tokens") {
if (change.type === 1) {
// Created
const token = change.obj;
setTokens((prevTokens) => [token, ...prevTokens]);
} else if (change.type === 2) {
// Updated
const token = change.obj;
setTokens((prevTokens) => {
const newTokens = [...prevTokens];
const i = newTokens.findIndex((t) => t.id === token.id);
if (i > -1) {
newTokens[i] = token;
}
return newTokens;
});
} else if (change.type === 3) {
// Deleted
const id = change.key;
setTokens((prevTokens) => {
const filtered = prevTokens.filter((token) => token.id !== id);
return filtered;
});
}
}
}
}
database.on("changes", handleTokenChanges);
return () => {
database.on("changes").unsubscribe(handleTokenChanges);
};
}, [database, databaseStatus]);
const ownedTokens = tokens.filter((token) => token.owner === userId);
const tokensById: { [key: string]: Token; } = tokens.reduce((obj: { [key: string]: Token }, token) => {
obj[token.id] = token;
return obj;
}, {});
setTokensById(
tokens.reduce((obj: Record<string, Token>, token: Token) => {
obj[token.id] = token;
return obj;
}, {})
);
}, [tokens]);
const value: TokenDataContext = {
tokens,
ownedTokens,
addToken,
removeToken,
tokenGroups,
removeTokens,
updateToken,
updateTokens,
putToken,
getToken,
tokensById,
tokensLoading,
getTokenFromDB,
loadTokens,
getToken,
updateTokenGroups,
updateTokensHidden,
};
return (