Typescript

This commit is contained in:
Mitchell McCaffrey 2021-07-17 17:25:41 +10:00
parent 97734a2f55
commit 2a053f4854
33 changed files with 412 additions and 419 deletions

View File

@ -58,7 +58,7 @@
"react-toast-notifications": "^2.4.3",
"react-use-gesture": "^9.1.3",
"shortid": "^2.2.15",
"simple-peer": "feross/simple-peer#694/head",
"simple-peer": "^9.11.0",
"simplebar-react": "^2.1.0",
"simplify-js": "^1.2.4",
"socket.io-client": "^4.1.2",
@ -110,7 +110,7 @@
"@types/react-router-dom": "^5.1.7",
"@types/react-select": "^4.0.17",
"@types/shortid": "^0.0.29",
"@types/simple-peer": "^9.6.3",
"@types/simple-peer": "^9.11.1",
"@types/uuid": "^8.3.1",
"typescript": "^4.2.4",
"worker-loader": "^3.0.8"

View File

@ -47,8 +47,8 @@ import { TokenDraggingOptions, TokenMenuOptions } from "../../types/Token";
import { Note, NoteDraggingOptions, NoteMenuOptions } from "../../types/Note";
type MapProps = {
map: Map;
mapState: MapState;
map: Map | null;
mapState: MapState | null;
mapActions: MapActions;
onMapTokenStateChange: TokenStateChangeEventHandler;
onMapTokenStateRemove: MapTokenStateRemoveHandler;
@ -66,7 +66,7 @@ type MapProps = {
allowFogDrawing: boolean;
allowMapChange: boolean;
allowNoteEditing: boolean;
disabledTokens: string[];
disabledTokens: Record<string, boolean>;
session: Session;
};
@ -244,9 +244,7 @@ function Map({
onRequestClose={() => setIsTokenMenuOpen(false)}
onTokenStateChange={onMapTokenStateChange}
tokenState={
tokenMenuOptions &&
mapState &&
mapState.tokens[tokenMenuOptions.tokenStateId]
tokenMenuOptions && mapState?.tokens[tokenMenuOptions.tokenStateId]
}
tokenImage={tokenMenuOptions && tokenMenuOptions.tokenImage}
map={map}
@ -372,9 +370,7 @@ function Map({
isOpen={isNoteMenuOpen}
onRequestClose={() => setIsNoteMenuOpen(false)}
onNoteChange={onMapNoteChange}
note={
noteMenuOptions && mapState && mapState.notes[noteMenuOptions.noteId]
}
note={noteMenuOptions && mapState?.notes[noteMenuOptions.noteId]}
noteNode={noteMenuOptions?.noteNode}
map={map}
/>

View File

@ -32,8 +32,8 @@ import { Settings } from "../../types/Settings";
type MapControlsProps = {
onMapChange: MapChangeEventHandler;
onMapReset: MapResetEventHandler;
currentMap?: Map;
currentMapState?: MapState;
currentMap: Map | null;
currentMapState: MapState | null;
selectedToolId: MapToolId;
onSelectedToolChange: (toolId: MapToolId) => void;
toolSettings: Settings;

View File

@ -37,7 +37,7 @@ export type DrawingAddEventHanlder = (drawing: Drawing) => void;
export type DrawingsRemoveEventHandler = (drawingIds: string[]) => void;
type MapDrawingProps = {
map: Map;
map: Map | null;
drawings: Drawing[];
onDrawingAdd: DrawingAddEventHanlder;
onDrawingsRemove: DrawingsRemoveEventHandler;
@ -85,7 +85,7 @@ function MapDrawing({
const mapStage = mapStageRef.current;
function getBrushPosition() {
if (!mapStage) {
if (!mapStage || !map) {
return;
}
const mapImage = mapStage.findOne("#mapImage");

View File

@ -20,7 +20,7 @@ import {
} from "../../types/Events";
type MapEditBarProps = {
currentMap?: Map;
currentMap: Map | null;
disabled: boolean;
onMapChange: MapChangeEventHandler;
onMapReset: MapResetEventHandler;
@ -79,7 +79,7 @@ function MapEditBar({
await removeMaps(selectedMapIds);
// Removed the map from the map screen if needed
if (currentMap && selectedMapIds.includes(currentMap.id)) {
onMapChange(undefined, undefined);
onMapChange(null, null);
}
onLoad(false);
}

View File

@ -58,7 +58,7 @@ type FogEditEventHandler = (edit: Partial<Fog>[]) => void;
type FogErrorEventHandler = (message: string) => void;
type MapFogProps = {
map: Map;
map: Map | null;
shapes: Fog[];
onShapesAdd: FogAddEventHandler;
onShapesCut: FogCutEventHandler;
@ -354,7 +354,7 @@ function MapFog({
x: mapWidth,
y: mapHeight,
});
if (map.snapToGrid) {
if (map?.snapToGrid) {
guides.push(
...getGuidesFromGridCell(
absoluteBrushPosition,

View File

@ -25,8 +25,8 @@ import { MapState } from "../../types/MapState";
type SelectedToolChangeEventHanlder = (tool: MapToolId) => void;
type MapInteractionProps = {
map: Map;
mapState: MapState;
map: Map | null;
mapState: MapState | null;
children?: React.ReactNode;
controls: React.ReactNode;
selectedToolId: MapToolId;

View File

@ -29,7 +29,7 @@ import { Map } from "../../types/Map";
import { PointsData } from "../../types/Drawing";
type MapMeasureProps = {
map: Map;
map: Map | null;
active: boolean;
};
@ -74,7 +74,7 @@ function MapMeasure({ map, active }: MapMeasureProps) {
if (!position) {
return;
}
if (map.snapToGrid) {
if (map?.snapToGrid) {
position = snapPositionToGrid(position);
}
return Vector2.divide(position, {

View File

@ -26,7 +26,7 @@ import {
const defaultNoteSize = 2;
type MapNoteProps = {
map: Map;
map: Map | null;
active: boolean;
onNoteAdd: NoteAddEventHander;
onNoteChange: NoteChangeEventHandler;
@ -75,7 +75,7 @@ function MapNotes({
if (!position) {
return;
}
if (map.snapToGrid) {
if (map?.snapToGrid) {
position = snapPositionToGrid(position);
}
return Vector2.divide(position, {

View File

@ -1,5 +1,3 @@
import React from "react";
import MapTile from "./MapTile";
import MapTileGroup from "./MapTileGroup";

View File

@ -18,7 +18,7 @@ type MapTokensProps = {
onMapTokenStateChange: TokenStateChangeEventHandler;
onTokenMenuOpen: TokenMenuOpenChangeEventHandler;
selectedToolId: MapToolId;
disabledTokens: string[];
disabledTokens: Record<string, boolean>;
};
function MapTokens({

View File

@ -16,8 +16,8 @@ import { MapState } from "../../types/MapState";
type SelectMapButtonProps = {
onMapChange: MapChangeEventHandler;
onMapReset: MapResetEventHandler;
currentMap?: Map;
currentMapState?: MapState;
currentMap: Map | null;
currentMapState: MapState | null;
disabled: boolean;
};

View File

@ -28,7 +28,7 @@ const defaultFontSize = 16;
type NoteProps = {
note: NoteType;
map: Map;
map: Map | null;
onNoteChange?: NoteChangeEventHandler;
onNoteMenuOpen?: NoteMenuOpenEventHandler;
draggable: boolean;
@ -72,20 +72,22 @@ function Note({
function handleDragMove(event: Konva.KonvaEventObject<DragEvent>) {
const noteGroup = event.target;
// Snap to corners of grid
if (map.snapToGrid) {
if (map?.snapToGrid) {
noteGroup.position(snapPositionToGrid(noteGroup.position()));
}
}
function handleDragEnd(event: Konva.KonvaEventObject<DragEvent>) {
const noteGroup = event.target;
onNoteChange?.({
...note,
x: noteGroup.x() / mapWidth,
y: noteGroup.y() / mapHeight,
lastModifiedBy: userId,
lastModified: Date.now(),
});
if (userId) {
onNoteChange?.({
...note,
x: noteGroup.x() / mapWidth,
y: noteGroup.y() / mapHeight,
lastModifiedBy: userId,
lastModified: Date.now(),
});
}
onNoteDragEnd?.(event, note.id);
setPreventMapInteraction(false);
}
@ -103,7 +105,7 @@ function Note({
if (draggable) {
setPreventMapInteraction(true);
}
if (note.locked && map.owner === userId) {
if (note.locked && map?.owner === userId) {
notePointerDownTimeRef.current = event.evt.timeStamp;
}
}
@ -114,7 +116,7 @@ function Note({
}
// Check note click when locked and we are the map owner
// We can't use onClick because that doesn't check pointer distance
if (note.locked && map.owner === userId) {
if (note.locked && map?.owner === userId) {
// If down and up time is small trigger a click
const delta = event.evt.timeStamp - notePointerDownTimeRef.current;
if (delta < 300) {

View File

@ -35,7 +35,7 @@ type NoteMenuProps = {
note?: Note;
noteNode?: Konva.Node;
onNoteChange: NoteChangeEventHandler;
map: Map;
map: Map | null;
};
function NoteMenu({

View File

@ -10,12 +10,6 @@ import SettingsButton from "../SettingsButton";
import StartTimerButton from "./StartTimerButton";
import Timer from "./Timer";
import DiceTrayButton from "./DiceTrayButton";
import {
PartyState,
PlayerDice,
PlayerInfo,
Timer as PartyTimer,
} from "./PartyState";
import useSetting from "../../hooks/useSetting";
@ -36,10 +30,10 @@ function Party({
onStreamEnd;
}) {
const setPlayerState = usePlayerUpdater();
const playerState: PlayerInfo = usePlayerState();
const partyState: PartyState = useParty();
const playerState = usePlayerState();
const partyState = useParty();
const [fullScreen] = useSetting("map.fullScreen");
const [fullScreen] = useSetting<boolean>("map.fullScreen");
const [shareDice, setShareDice] = useSetting("dice.shareDice");
function handleTimerStart(newTimer: PartyTimer) {

View File

@ -6,7 +6,7 @@ import Slider from "../Slider";
import MapMenu from "../map/MapMenu";
import colors, { colorOptions } from "../../helpers/colors";
import colors, { Color, colorOptions } from "../../helpers/colors";
import usePrevious from "../../hooks/usePrevious";
@ -27,10 +27,10 @@ import { Map } from "../../types/Map";
type TokenMenuProps = {
isOpen: boolean;
onRequestClose: RequestCloseEventHandler;
tokenState: TokenState;
tokenImage: Konva.Node;
tokenState?: TokenState;
tokenImage?: Konva.Node;
onTokenStateChange: TokenStateChangeEventHandler;
map: Map;
map: Map | null;
};
const defaultTokenMaxSize = 6;
@ -74,7 +74,7 @@ function TokenMenu({
tokenState && onTokenStateChange({ [tokenState.id]: { label: label } });
}
function handleStatusChange(status: string) {
function handleStatusChange(status: Color) {
if (!tokenState) {
return;
}

View File

@ -209,8 +209,8 @@ export function AssetURLsProvider({ children }: { children: React.ReactNode }) {
* Helper function to load either file or default asset into a URL
*/
export function useAssetURL(
assetId: string,
type: "file" | "default",
assetId: string | null,
type: "file" | "default" | null,
defaultSources: Record<string, string>,
unknownSource?: string
) {
@ -227,9 +227,8 @@ export function useAssetURL(
if (!assetId || type !== "file") {
return;
}
function updateAssetURL() {
function increaseReferences(prevURLs: AssetURLs): AssetURLs {
const updateAssetURL = () => {
const increaseReferences = (prevURLs: AssetURLs): AssetURLs => {
return {
...prevURLs,
[assetId]: {
@ -237,14 +236,14 @@ export function useAssetURL(
references: prevURLs[assetId].references + 1,
},
};
}
};
function createReference(prevURLs: AssetURLs): AssetURLs {
const createReference = (prevURLs: AssetURLs): AssetURLs => {
return {
...prevURLs,
[assetId]: { url: null, id: assetId, references: 1 },
};
}
};
setAssetURLs?.((prevURLs) => {
if (assetId in prevURLs) {
// Check if the asset url is already added and increase references
@ -253,7 +252,7 @@ export function useAssetURL(
return createReference(prevURLs);
}
});
}
};
updateAssetURL();
@ -307,22 +306,22 @@ type DefaultData = {
* Load a map or token into a URL taking into account a thumbnail and multiple resolutions
*/
export function useDataURL(
data: FileData | DefaultData,
data: FileData | DefaultData | null,
defaultSources: Record<string, string>,
unknownSource: string | undefined = undefined,
thumbnail = false
) {
const [assetId, setAssetId] = useState<string>();
const [assetId, setAssetId] = useState<string | null>(null);
useEffect(() => {
if (!data) {
return;
}
function loadAssetId() {
if (!data) {
return;
}
if (data.type === "default") {
setAssetId(data.key);
} else {
if (thumbnail) {
if (thumbnail && data.thumbnail) {
setAssetId(data.thumbnail);
} else if (
data.resolutions &&
@ -340,8 +339,8 @@ export function useDataURL(
}, [data, thumbnail]);
const assetURL = useAssetURL(
assetId || "",
data?.type,
assetId || null,
data?.type || null,
defaultSources,
unknownSource
);

View File

@ -8,7 +8,7 @@ import { getRandomMonster } from "../helpers/monsters";
import useNetworkedState, {
SetNetworkedState,
} from "../hooks/useNetworkedState";
import Session from "../network/Session";
import Session, { SessionStatus } from "../network/Session";
import { PlayerState } from "../types/PlayerState";
export const PlayerStateContext =
@ -103,7 +103,7 @@ export function PlayerProvider({ session, children }: PlayerProviderProps) {
updateSessionId();
}
function handleSocketStatus(status: string) {
function handleSocketStatus(status: SessionStatus) {
if (status === "joined") {
updateSessionId();
}

View File

@ -245,10 +245,10 @@ export function getGridUpdatedInset(
/**
* Get the max zoom for a grid
* @param {Grid} grid
* @param {Grid=} grid
* @returns {number}
*/
export function getGridMaxZoom(grid: Grid): number {
export function getGridMaxZoom(grid?: Grid): number {
if (!grid) {
return 10;
}

View File

@ -11,7 +11,7 @@ type ImageData = {
};
function useImageCenter(
data: ImageData,
data: ImageData | null,
stageRef: MapStage,
stageWidth: number,
stageHeight: number,

View File

@ -8,7 +8,7 @@ import { mapSources as defaultMapSources } from "../maps";
import { Map } from "../types/Map";
function useMapImage(
map: Map
map: Map | null
): [HTMLImageElement | undefined, "loaded" | "loading" | "failed"] {
const mapURL = useDataURL(map, defaultMapSources);
const [mapImage, mapImageStatus] = useImage(mapURL || "");

View File

@ -42,7 +42,7 @@ type SelectMapProps = {
onDone: RequestCloseEventHandler;
onMapChange: MapChangeEventHandler;
onMapReset: MapResetEventHandler;
currentMap?: Map;
currentMap: Map | null;
};
function SelectMapModal({
@ -175,12 +175,12 @@ function SelectMapModal({
}
if (mapId) {
setIsLoading(true);
const map = await getMap(mapId);
const mapState = await getMapState(mapId);
const map = (await getMap(mapId)) || null;
const mapState = (await getMapState(mapId)) || null;
onMapChange(map, mapState);
setIsLoading(false);
} else {
onMapChange(undefined, undefined);
onMapChange(null, null);
}
onDone();
}

View File

@ -8,21 +8,37 @@ import blobToBuffer from "../helpers/blobToBuffer";
// http://viblast.com/blog/2015/2/5/webrtc-data-channel-message-size/
const MAX_BUFFER_SIZE = 16000;
class Connection extends SimplePeer {
currentChunks;
dataChannels;
type NetworkChunk = {
__chunked: boolean;
data: Uint8Array;
id: string;
index: number;
total: number;
};
constructor(props) {
type LocalChunk = {
data: Uint8Array[];
count: number;
total: number;
};
export type DataProgressEvent = {
id: string;
count: number;
total: number;
};
class Connection extends SimplePeer {
currentChunks: Record<string, LocalChunk>;
constructor(props: SimplePeer.Options) {
super(props);
this.currentChunks = {};
this.dataChannels = {};
this.on("data", this.handleData);
this.on("datachannel", this.handleDataChannel);
}
// Intercept the data event with decoding and chunking support
handleData(packed) {
const unpacked = decode(packed);
handleData(packed: Uint8Array) {
const unpacked = decode(packed) as NetworkChunk;
// If the special property __chunked is set and true
// The data is a partial chunk of the a larger file
// So wait until all chunks are collected and assembled
@ -46,7 +62,6 @@ class Connection extends SimplePeer {
// All chunks have been loaded
if (chunk.count === chunk.total) {
// Merge chunks with a blob
// TODO: Look at a more efficient way to recombine buffer data
const merged = new Blob(chunk.data);
blobToBuffer(merged).then((buffer) => {
this.emit("dataComplete", decode(buffer));
@ -62,52 +77,23 @@ class Connection extends SimplePeer {
* Custom send function with encoding, chunking and data channel support
* Uses `write` to send the data to allow for buffer / backpressure handling
* @param {any} object
* @param {string=} channel
* @param {string=} chunkId Optional ID to use for chunking
*/
sendObject(object, channel?: string, chunkId?: string) {
sendObject(object: any, chunkId?: string) {
try {
const packedData = encode(object);
const chunks = this.chunk(packedData, chunkId);
for (let chunk of chunks) {
if (this.dataChannels[channel]) {
this.dataChannels[channel].write(encode(chunk));
} else {
this.write(encode(chunk));
}
this.write(encode(chunk));
}
} catch (error) {
console.error(error);
}
}
// Override the create data channel function to store our own named reference to it
// and to use our custom data handler
createDataChannel(channelName: string, channelConfig, opts) {
// TODO: resolve createDataChannel
// @ts-ignore
const channel = super.createDataChannel(channelName, channelConfig, opts);
this.handleDataChannel(channel);
return channel;
}
handleDataChannel(channel) {
const channelName = channel.channelName;
this.dataChannels[channelName] = channel;
channel.on("data", this.handleData.bind(this));
channel.on("error", (error) => {
this.emit("error", error);
});
}
// Converted from https://github.com/peers/peerjs/
/**
* Chunk byte array
* @param {Uint8Array} data
* @param {string=} chunkId
* @returns {Uint8Array[]}
*/
chunk(data: Uint8Array, chunkId?: string) {
/** Chunk byte array */
chunk(data: Uint8Array, chunkId?: string): NetworkChunk[] {
const chunks = [];
const size = data.byteLength;
const total = Math.ceil(size / MAX_BUFFER_SIZE);

View File

@ -14,7 +14,7 @@ import useDebounce from "../hooks/useDebounce";
import useNetworkedState from "../hooks/useNetworkedState";
// Load session for auto complete
import Session from "./Session";
import Session, { PeerDataEvent, PeerDataProgressEvent } from "./Session";
import Action from "../actions/Action";
@ -38,6 +38,7 @@ import {
import { TokenState } from "../types/TokenState";
import { DrawingState } from "../types/Drawing";
import { FogState } from "../types/Fog";
import { Note } from "../types/Note";
const defaultMapActions: MapActions = {
mapDrawActions: [],
@ -204,7 +205,10 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
}
}, [currentMap, debouncedMapState, userId, database, updateMapState]);
async function handleMapChange(newMap, newMapState) {
async function handleMapChange(
newMap: MapType | null,
newMapState: MapState | null
) {
// Clear map before sending new one
setCurrentMap(null);
session.socket?.emit("map", null);
@ -222,7 +226,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
await loadAssetManifestFromMap(newMap, newMapState);
}
function handleMapReset(newMapState) {
function handleMapReset(newMapState: MapState) {
setCurrentMapState(newMapState, true, true);
setMapActions(defaultMapActions);
}
@ -264,7 +268,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
}
function updateActionIndex(
change,
change: number,
indexKey: MapActionsIndexKey,
actionsKey: MapActionsKey,
shapesKey: "drawShapes" | "fogShapes"
@ -288,19 +292,21 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
// Redo
for (let i = prevIndex + 1; i < newIndex + 1; i++) {
let action = mapActions[actionsKey][i];
shapes = action.execute(shapes);
shapes = action.execute(shapes as any);
}
} else {
// Undo
for (let i = prevIndex; i > newIndex; i--) {
let action = mapActions[actionsKey][i];
shapes = action.undo(shapes);
shapes = action.undo(shapes as any);
}
}
return {
...prevMapState,
[shapesKey]: shapes,
};
} else {
return prevMapState;
}
});
@ -342,7 +348,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
}
// If map changes clear map actions
const previousMapIdRef = useRef();
const previousMapIdRef = useRef<string>();
useEffect(() => {
if (currentMap && currentMap?.id !== previousMapIdRef.current) {
setMapActions(defaultMapActions);
@ -350,21 +356,31 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
}
}, [currentMap]);
function handleNoteChange(note) {
setCurrentMapState((prevMapState) => ({
...prevMapState,
notes: {
...prevMapState.notes,
[note.id]: note,
},
}));
function handleNoteChange(note: Note) {
setCurrentMapState((prevMapState) => {
if (!prevMapState) {
return prevMapState;
}
return {
...prevMapState,
notes: {
...prevMapState.notes,
[note.id]: note,
},
};
});
}
function handleNoteRemove(noteId: string) {
setCurrentMapState((prevMapState) => ({
...prevMapState,
notes: omit(prevMapState.notes, [noteId]),
}));
setCurrentMapState((prevMapState) => {
if (!prevMapState) {
return prevMapState;
}
return {
...prevMapState,
notes: omit(prevMapState.notes, [noteId]),
};
});
}
/**
@ -387,6 +403,9 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
}
setCurrentMapState((prevMapState) => {
if (!prevMapState) {
return prevMapState;
}
let newMapTokens = { ...prevMapState.tokens };
for (let tokenState of tokenStates) {
newMapTokens[tokenState.id] = tokenState;
@ -395,15 +414,20 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
});
}
function handleMapTokenStateChange(change) {
function handleMapTokenStateChange(
change: Record<string, Partial<TokenState>>
) {
if (!currentMapState) {
return;
}
setCurrentMapState((prevMapState) => {
if (!prevMapState) {
return prevMapState;
}
let tokens = { ...prevMapState.tokens };
for (let id in change) {
if (id in tokens) {
tokens[id] = { ...tokens[id], ...change[id] };
tokens[id] = { ...tokens[id], ...change[id] } as TokenState;
}
}
@ -414,29 +438,24 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
});
}
function handleMapTokenStateRemove(tokenState) {
function handleMapTokenStateRemove(tokenState: TokenState) {
setCurrentMapState((prevMapState) => {
if (!prevMapState) {
return prevMapState;
}
const { [tokenState.id]: old, ...rest } = prevMapState.tokens;
return { ...prevMapState, tokens: rest };
});
}
useEffect(() => {
async function handlePeerData({
id,
data,
reply,
}: {
id: string;
data;
reply;
}) {
async function handlePeerData({ id, data, reply }: PeerDataEvent) {
if (id === "assetRequest") {
const asset = await getAsset(data.id);
if (asset) {
reply("assetResponseSuccess", asset, undefined, data.id);
reply("assetResponseSuccess", asset, data.id);
} else {
reply("assetResponseFail", data.id, undefined, data.id);
reply("assetResponseFail", data.id, data.id);
}
}
@ -456,15 +475,11 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
id,
total,
count,
}: {
id: string;
total: number;
count: number;
}) {
}: PeerDataProgressEvent) {
assetProgressUpdate({ id, total, count });
}
async function handleSocketMap(map) {
async function handleSocketMap(map?: MapType) {
if (map) {
setCurrentMap(map);
} else {
@ -502,7 +517,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
(currentMapState.editFlags.includes("notes") ||
currentMap?.owner === userId);
const disabledMapTokens = {};
const disabledMapTokens: Record<string, boolean> = {};
// If we have a map and state and have the token permission disabled
// and are not the map owner
if (
@ -539,10 +554,10 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
onFogDrawRedo={handleFogDrawRedo}
onMapNoteChange={handleNoteChange}
onMapNoteRemove={handleNoteRemove}
allowMapDrawing={canEditMapDrawing}
allowFogDrawing={canEditFogDrawing}
allowMapDrawing={!!canEditMapDrawing}
allowFogDrawing={!!canEditFogDrawing}
allowMapChange={canChangeMap}
allowNoteEditing={canEditNotes}
allowNoteEditing={!!canEditNotes}
disabledTokens={disabledMapTokens}
session={session}
/>

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from "react";
import { useState, useEffect, useRef } from "react";
import { Group } from "react-konva";
import { useUserId } from "../contexts/UserIdContext";
@ -9,20 +9,31 @@ import Vector2 from "../helpers/Vector2";
import useSetting from "../hooks/useSetting";
import Session from "./Session";
import { Color } from "../helpers/colors";
import { PointerState } from "../types/Pointer";
// Send pointer updates every 50ms (20fps)
const sendTickRate = 50;
function NetworkedMapPointer({
session,
active,
}: {
type InterpolatedPointerState = PointerState & { time: number };
type PointerInterpolation = {
id: string;
from: InterpolatedPointerState | null;
to: InterpolatedPointerState;
};
type NetworkedMapPointerProps = {
session: Session;
active: boolean;
}) {
};
function NetworkedMapPointer({ session, active }: NetworkedMapPointerProps) {
const userId = useUserId();
const [localPointerState, setLocalPointerState] = useState({});
const [pointerColor] = useSetting("pointer.color");
const [localPointerState, setLocalPointerState] = useState<
Record<string, PointerState>
>({});
const [pointerColor] = useSetting<Color>("pointer.color");
const sessionRef = useRef(session);
useEffect(() => {
@ -45,14 +56,12 @@ function NetworkedMapPointer({
// Send pointer updates every sendTickRate to peers to save on bandwidth
// We use requestAnimationFrame as setInterval was being blocked during
// re-renders on Chrome with Windows
const ownPointerUpdateRef: React.MutableRefObject<
{ position; visible: boolean; id; color } | undefined | null
> = useRef();
const ownPointerUpdateRef = useRef<PointerState | null>(null);
useEffect(() => {
let prevTime = performance.now();
let request = requestAnimationFrame(update);
let counter = 0;
function update(time) {
function update(time: number) {
request = requestAnimationFrame(update);
const deltaTime = time - prevTime;
counter += deltaTime;
@ -79,7 +88,10 @@ function NetworkedMapPointer({
};
}, []);
function updateOwnPointerState(position, visible: boolean) {
function updateOwnPointerState(position: Vector2, visible: boolean) {
if (!userId) {
return;
}
setLocalPointerState((prev) => ({
...prev,
[userId]: { position, visible, id: userId, color: pointerColor },
@ -92,23 +104,23 @@ function NetworkedMapPointer({
};
}
function handleOwnPointerDown(position) {
function handleOwnPointerDown(position: Vector2) {
updateOwnPointerState(position, true);
}
function handleOwnPointerMove(position) {
function handleOwnPointerMove(position: Vector2) {
updateOwnPointerState(position, true);
}
function handleOwnPointerUp(position) {
function handleOwnPointerUp(position: Vector2) {
updateOwnPointerState(position, false);
}
// Handle pointer data receive
const interpolationsRef: React.MutableRefObject = useRef({});
const interpolationsRef = useRef<Record<string, PointerInterpolation>>({});
useEffect(() => {
// TODO: Handle player disconnect while pointer visible
function handleSocketPlayerPointer(pointer) {
function handleSocketPlayerPointer(pointer: InterpolatedPointerState) {
const interpolations = interpolationsRef.current;
const id = pointer.id;
if (!(id in interpolations)) {
@ -154,7 +166,7 @@ function NetworkedMapPointer({
function animate() {
request = requestAnimationFrame(animate);
const time = performance.now();
let interpolatedPointerState = {};
let interpolatedPointerState: Record<string, PointerState> = {};
for (let interp of Object.values(interpolationsRef.current)) {
if (!interp.from || !interp.to) {
continue;
@ -206,9 +218,13 @@ function NetworkedMapPointer({
active={pointer.id === userId ? active : false}
position={pointer.position}
visible={pointer.visible}
onPointerDown={pointer.id === userId && handleOwnPointerDown}
onPointerMove={pointer.id === userId && handleOwnPointerMove}
onPointerUp={pointer.id === userId && handleOwnPointerUp}
onPointerDown={
pointer.id === userId ? handleOwnPointerDown : undefined
}
onPointerMove={
pointer.id === userId ? handleOwnPointerMove : undefined
}
onPointerUp={pointer.id === userId ? handleOwnPointerUp : undefined}
color={pointer.color}
/>
))}

View File

@ -1,14 +1,12 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useToasts } from "react-toast-notifications";
// Load session for auto complete
import Session, { SessionPeer } from "./Session";
import Session, { PeerTrackAddedEvent, PeerTrackRemovedEvent } from "./Session";
import { isStreamStopped, omit } from "../helpers/shared";
import { useParty } from "../contexts/PartyContext";
import Party from "../components/party/Party";
import { PartyState } from "../components/party/PartyState";
/**
* @typedef {object} NetworkedPartyProps
@ -16,13 +14,13 @@ import { PartyState } from "../components/party/PartyState";
* @property {Session} session
*/
type NetworkedPartyProps = { gameId: string, session: Session }
type NetworkedPartyProps = { gameId: string; session: Session };
/**
* @param {NetworkedPartyProps} props
*/
function NetworkedParty(props: NetworkedPartyProps) {
const partyState: PartyState = useParty();
function NetworkedParty({ gameId, session }: NetworkedPartyProps) {
const partyState = useParty();
const [stream, setStream] = useState<MediaStream | null>(null);
const [partyStreams, setPartyStreams] = useState({});
@ -35,7 +33,9 @@ function NetworkedParty(props: NetworkedPartyProps) {
// Only add the audio track of the stream to the remote peer
if (track.kind === "audio") {
for (let player of Object.values(partyState)) {
props.session.startStreamTo(player.sessionId, track, localStream);
if (player.sessionId) {
session.startStreamTo(player.sessionId, track, localStream);
}
}
}
}
@ -50,12 +50,14 @@ function NetworkedParty(props: NetworkedPartyProps) {
// Only sending audio so only remove the audio track
if (track.kind === "audio") {
for (let player of Object.values(partyState)) {
props.session.endStreamTo(player.sessionId, track, localStream);
if (player.sessionId) {
session.endStreamTo(player.sessionId, track, localStream);
}
}
}
}
},
[props.session, partyState]
[session, partyState]
);
// Keep a reference to players who have just joined to show the joined notification
@ -77,7 +79,7 @@ function NetworkedParty(props: NetworkedPartyProps) {
const tracks = stream.getTracks();
for (let track of tracks) {
if (track.kind === "audio") {
props.session.startStreamTo(sessionId, track, stream);
session.startStreamTo(sessionId, track, stream);
}
}
}
@ -92,14 +94,20 @@ function NetworkedParty(props: NetworkedPartyProps) {
}
}
function handlePeerTrackAdded({ peer, stream: remoteStream }: { peer: SessionPeer, stream: MediaStream}) {
function handlePeerTrackAdded({
peer,
stream: remoteStream,
}: PeerTrackAddedEvent) {
setPartyStreams((prevStreams) => ({
...prevStreams,
[peer.id]: remoteStream,
}));
}
function handlePeerTrackRemoved({ peer, stream: remoteStream }: { peer: SessionPeer, stream: MediaStream }) {
function handlePeerTrackRemoved({
peer,
stream: remoteStream,
}: PeerTrackRemovedEvent) {
if (isStreamStopped(remoteStream)) {
setPartyStreams((prevStreams) => omit(prevStreams, [peer.id]));
} else {
@ -110,16 +118,16 @@ function NetworkedParty(props: NetworkedPartyProps) {
}
}
props.session.on("playerJoined", handlePlayerJoined);
props.session.on("playerLeft", handlePlayerLeft);
props.session.on("peerTrackAdded", handlePeerTrackAdded);
props.session.on("peerTrackRemoved", handlePeerTrackRemoved);
session.on("playerJoined", handlePlayerJoined);
session.on("playerLeft", handlePlayerLeft);
session.on("peerTrackAdded", handlePeerTrackAdded);
session.on("peerTrackRemoved", handlePeerTrackRemoved);
return () => {
props.session.off("playerJoined", handlePlayerJoined);
props.session.off("playerLeft", handlePlayerLeft);
props.session.off("peerTrackAdded", handlePeerTrackAdded);
props.session.off("peerTrackRemoved", handlePeerTrackRemoved);
session.off("playerJoined", handlePlayerJoined);
session.off("playerLeft", handlePlayerLeft);
session.off("peerTrackAdded", handlePeerTrackAdded);
session.off("peerTrackRemoved", handlePeerTrackRemoved);
};
});
@ -142,7 +150,7 @@ function NetworkedParty(props: NetworkedPartyProps) {
return (
<>
<Party
gameId={props.gameId}
gameId={gameId}
onStreamStart={handleStreamStart}
onStreamEnd={handleStreamEnd}
stream={stream}

View File

@ -2,14 +2,13 @@ import io, { Socket } from "socket.io-client";
import msgParser from "socket.io-msgpack-parser";
import { EventEmitter } from "events";
import Connection from "./Connection";
import Connection, { DataProgressEvent } from "./Connection";
import { omit } from "../helpers/shared";
import { logError } from "../helpers/logging";
import { SimplePeerData } from "simple-peer";
import { SignalData } from "simple-peer";
/**
* @typedef {object} SessionPeer
* @property {string} id - The socket id of the peer
* @property {Connection} connection - The actual peer connection
* @property {boolean} initiator - Is this peer the initiator of the connection
@ -22,37 +21,13 @@ export type SessionPeer = {
ready: boolean;
};
/**
* @callback peerReply
* @param {string} id - The id of the event
* @param {object} data - The data to send
* @param {string=} channel - The channel to send to
* @param {string=} chunkId
*/
export type PeerData = any;
type peerReply = (id: string, data: SimplePeerData, channel: string) => void;
/**
* Session Status Event - Status of the session has changed
*
* @event Session#status
* @property {"ready"|"joining"|"joined"|"offline"|"reconnecting"|"auth"|"needs_update"} status
*/
export type PeerReply = (id: string, data: PeerData, chunkId?: string) => void;
/**
*
* Handles connections to multiple peers
*
* @fires Session#peerConnect
* @fires Session#peerData
* @fires Session#peerTrackAdded
* @fires Session#peerTrackRemoved
* @fires Session#peerDisconnect
* @fires Session#peerError
* @fires Session#status
* @fires Session#playerJoined
* @fires Session#playerLeft
* @fires Session#gameExpired
*/
class Session extends EventEmitter {
/**
@ -73,7 +48,7 @@ class Session extends EventEmitter {
return this.socket && this.socket.id;
}
_iceServers: string[] = [];
_iceServers: RTCIceServer[] = [];
// Store party id and password for reconnect
_gameId: string = "";
@ -133,19 +108,10 @@ class Session extends EventEmitter {
/**
* Send data to a single peer
*
* @param {string} sessionId - The socket id of the player to send to
* @param {string} eventId - The id of the event to send
* @param {object} data
* @param {string=} channel
* @param {string=} chunkId
* @param sessionId - The socket id of the player to send to
* @param eventId - The id of the event to send
*/
sendTo(
sessionId: string,
eventId: string,
data,
channel?: string,
chunkId?: string
) {
sendTo(sessionId: string, eventId: string, data: PeerData, chunkId?: string) {
if (!(sessionId in this.peers)) {
if (!this._addPeer(sessionId, true)) {
return;
@ -156,14 +122,13 @@ class Session extends EventEmitter {
this.peers[sessionId].connection.once("connect", () => {
this.peers[sessionId].connection.sendObject(
{ id: eventId, data },
channel,
chunkId
);
});
} else {
this.peers[sessionId].connection.sendObject(
{ id: eventId, data },
channel
chunkId
);
}
}
@ -241,7 +206,7 @@ class Session extends EventEmitter {
* @param {boolean} initiator
* @returns {boolean} True if peer was added successfully
*/
_addPeer(id: string, initiator: boolean) {
_addPeer(id: string, initiator: boolean): boolean {
try {
const connection = new Connection({
initiator,
@ -254,11 +219,11 @@ class Session extends EventEmitter {
const peer = { id, connection, initiator, ready: false };
function reply(id: string, data, channel?: string, chunkId?: string) {
peer.connection.sendObject({ id, data }, channel, chunkId);
}
const reply: PeerReply = (id, data, chunkId) => {
peer.connection.sendObject({ id, data }, chunkId);
};
const handleSignal = (signal) => {
const handleSignal = (signal: SignalData) => {
this.socket.emit("signal", JSON.stringify({ to: peer.id, signal }));
};
@ -266,105 +231,54 @@ class Session extends EventEmitter {
if (peer.id in this.peers) {
this.peers[peer.id].ready = true;
}
/**
* Peer Connect Event - A peer has connected
*
* @event Session#peerConnect
* @type {object}
* @property {SessionPeer} peer
* @property {peerReply} reply
*/
this.emit("peerConnect", { peer, reply });
const peerConnectEvent: PeerConnectEvent = {
peer,
reply,
};
this.emit("peerConnect", peerConnectEvent);
};
const handleDataComplete = (data) => {
/**
* Peer Data Event - Data received by a peer
*
* @event Session#peerData
* @type {object}
* @property {SessionPeer} peer
* @property {string} id
* @property {object} data
* @property {peerReply} reply
*/
let peerDataEvent: {
peer: SessionPeer;
id: string;
data;
reply: peerReply;
} = {
const handleDataComplete = (data: any) => {
const peerDataEvent: PeerDataEvent = {
peer,
id: data.id,
data: data.data,
reply: reply,
};
console.log(`Data: ${JSON.stringify(data)}`);
this.emit("peerData", peerDataEvent);
};
const handleDataProgress = ({
id,
count,
total,
}: {
id: string;
count: number;
total: number;
}) => {
this.emit("peerDataProgress", {
const handleDataProgress = ({ id, count, total }: DataProgressEvent) => {
const peerDataProgressEvent: PeerDataProgressEvent = {
peer,
id,
count,
total,
reply,
});
};
this.emit("peerDataProgress", peerDataProgressEvent);
};
const handleTrack = (track: MediaStreamTrack, stream: MediaStream) => {
/**
* Peer Track Added Event - A `MediaStreamTrack` was added by a peer
*
* @event Session#peerTrackAdded
* @type {object}
* @property {SessionPeer} peer
* @property {MediaStreamTrack} track
* @property {MediaStream} stream
*/
let peerTrackAddedEvent: {
peer: SessionPeer;
track: MediaStreamTrack;
stream: MediaStream;
} = { peer, track, stream };
const peerTrackAddedEvent: PeerTrackAddedEvent = {
peer,
track,
stream,
};
this.emit("peerTrackAdded", peerTrackAddedEvent);
track.addEventListener("mute", () => {
/**
* Peer Track Removed Event - A `MediaStreamTrack` was removed by a peer
*
* @event Session#peerTrackRemoved
* @type {object}
* @property {SessionPeer} peer
* @property {MediaStreamTrack} track
* @property {MediaStream} stream
*/
let peerTrackRemovedEvent: {
peer: SessionPeer;
track: MediaStreamTrack;
stream: MediaStream;
} = { peer, track, stream };
const peerTrackRemovedEvent: PeerTrackRemovedEvent = {
peer,
track,
stream,
};
this.emit("peerTrackRemoved", peerTrackRemovedEvent);
});
};
const handleClose = () => {
/**
* Peer Disconnect Event - A peer has disconnected
*
* @event Session#peerDisconnect
* @type {object}
* @property {SessionPeer} peer
*/
let peerDisconnectEvent: { peer: SessionPeer } = { peer };
const peerDisconnectEvent: PeerDisconnectEvent = { peer };
this.emit("peerDisconnect", peerDisconnectEvent);
if (peer.id in this.peers) {
peer.connection.destroy();
@ -372,16 +286,8 @@ class Session extends EventEmitter {
}
};
const handleError = (error: Error) => {
/**
* Peer Error Event - An error occured with a peer connection
*
* @event Session#peerError
* @type {object}
* @property {SessionPeer} peer
* @property {Error} error
*/
let peerErrorEvent: { peer: SessionPeer; error: Error } = {
const handleError = (error: PeerError) => {
const peerErrorEvent: PeerErrorEvent = {
peer,
error,
};
@ -418,31 +324,14 @@ class Session extends EventEmitter {
}
_handleGameExpired() {
/**
* Game Expired Event - A joining game has expired
*
* @event Session#gameExpired
*/
this.emit("gameExpired");
}
_handlePlayerJoined(id: string) {
/**
* Player Joined Event - A player has joined the game
*
* @event Session#playerJoined
* @property {string} id
*/
this.emit("playerJoined", id);
}
_handlePlayerLeft(id: string) {
/**
* Player Left Event - A player has left the game
*
* @event Session#playerLeft
* @property {string} id
*/
this.emit("playerLeft", id);
if (id in this.peers) {
this.peers[id].connection.destroy();
@ -450,7 +339,7 @@ class Session extends EventEmitter {
}
}
_handleSignal(data) {
_handleSignal(data: { from: string; signal: SignalData }) {
const { from, signal } = data;
if (!(from in this.peers)) {
if (!this._addPeer(from, false)) {
@ -484,14 +373,96 @@ class Session extends EventEmitter {
}
_handleForceUpdate() {
/**
* Force Update Event - An update has been released
*
* @event Session#forceUpdate
*/
this.socket.disconnect();
this.emit("status", "needs_update");
}
}
export type PeerConnectEvent = {
peer: SessionPeer;
reply: PeerReply;
};
export type PeerConnectEventHandler = (event: PeerConnectEvent) => void;
export type PeerDataEvent = {
peer: SessionPeer;
id: string;
data: PeerData;
reply: PeerReply;
};
export type PeerDataEventHandler = (event: PeerDataEvent) => void;
export type PeerDataProgressEvent = {
peer: SessionPeer;
id: string;
count: number;
total: number;
reply: PeerReply;
};
export type PeerDataProgressEventHandler = (
event: PeerDataProgressEvent
) => void;
export type PeerTrackAddedEvent = {
peer: SessionPeer;
track: MediaStreamTrack;
stream: MediaStream;
};
export type PeerTrackAddedEventHandler = (event: PeerTrackAddedEvent) => void;
export type PeerTrackRemovedEvent = {
peer: SessionPeer;
track: MediaStreamTrack;
stream: MediaStream;
};
export type PeerTrackRemovedEventHandler = (
event: PeerTrackRemovedEvent
) => void;
export type PeerDisconnectEvent = { peer: SessionPeer };
export type PeerDisconnectEventHandler = (event: PeerDisconnectEvent) => void;
export type PeerError = Error & { code: string };
export type PeerErrorEvent = { peer: SessionPeer; error: PeerError };
export type PeerErrorEventHandler = (event: PeerErrorEvent) => void;
export type SessionStatus =
| "ready"
| "joining"
| "joined"
| "offline"
| "reconnecting"
| "auth"
| "needs_update";
export type SessionStatusHandler = (status: SessionStatus) => void;
export type PlayerJoinedHandler = (id: string) => void;
export type PlayerLeftHandler = (id: string) => void;
export type GameExpiredHandler = () => void;
declare interface Session {
/** Peer Connect Event - A peer has connected */
on(event: "peerConnect", listener: PeerConnectEventHandler): this;
/** Peer Data Event - Data received by a peer */
on(event: "peerData", listener: PeerDataEventHandler): this;
/** Peer Data Progress Event - Part of some data received by a peer */
on(event: "peerDataProgress", listener: PeerDataProgressEventHandler): this;
/** Peer Track Added Event - A `MediaStreamTrack` was added by a peer */
on(event: "peerTrackAdded", listener: PeerTrackAddedEventHandler): this;
/** Peer Track Removed Event - A `MediaStreamTrack` was removed by a peer */
on(event: "peerTrackRemoved", listener: PeerTrackRemovedEventHandler): this;
/** Peer Disconnect Event - A peer has disconnected */
on(event: "peerDisconnect", listener: PeerDisconnectEventHandler): this;
/** Peer Error Event - An error occured with a peer connection */
on(event: "peerError", listener: PeerErrorEventHandler): this;
/** Session Status Event - Status of the session has changed */
on(event: "status", listener: SessionStatusHandler): this;
/** Player Joined Event - A player has joined the game */
on(event: "playerJoined", listener: PlayerJoinedHandler): this;
/** Player Left Event - A player has left the game */
on(event: "playerLeft", listener: PlayerLeftHandler): this;
/** Game Expired Event - A joining game has expired */
on(event: "gameExpired", listener: GameExpiredHandler): this;
}
export default Session;

View File

@ -28,7 +28,7 @@ import { MapLoadingProvider } from "../contexts/MapLoadingContext";
import NetworkedMapAndTokens from "../network/NetworkedMapAndTokens";
import NetworkedParty from "../network/NetworkedParty";
import Session from "../network/Session";
import Session, { PeerErrorEvent, SessionStatus } from "../network/Session";
function Game() {
const { id: gameId }: { id: string } = useParams();
@ -36,7 +36,7 @@ function Game() {
const { databaseStatus } = useDatabase();
const [session] = useState(new Session());
const [sessionStatus, setSessionStatus] = useState();
const [sessionStatus, setSessionStatus] = useState<SessionStatus>();
useEffect(() => {
async function connect() {
@ -50,9 +50,9 @@ function Game() {
}, [session]);
// Handle session errors
const [peerError, setPeerError] = useState(null);
const [peerError, setPeerError] = useState<string | null>(null);
useEffect(() => {
function handlePeerError({ error }) {
function handlePeerError({ error }: PeerErrorEvent) {
if (error.code === "ERR_WEBRTC_SUPPORT") {
setPeerError("WebRTC not supported.");
} else if (error.code === "ERR_CREATE_OFFER") {
@ -66,7 +66,7 @@ function Game() {
}, [session]);
useEffect(() => {
function handleStatus(status: any) {
function handleStatus(status: SessionStatus) {
setSessionStatus(status);
}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { Flex, Button, Image, Text, IconButton, Link } from "theme-ui";
import { useHistory } from "react-router-dom";
@ -23,9 +23,8 @@ import owlington from "../images/Owlington.png";
function Home() {
const [isStartModalOpen, setIsStartModalOpen] = useState(false);
const [isJoinModalOpen, setIsJoinModalOpen] = useState(false);
const [isGettingStartedModalOpen, setIsGettingStartedModalOpen] = useState(
false
);
const [isGettingStartedModalOpen, setIsGettingStartedModalOpen] =
useState(false);
// Reset password on visiting home
const { setPassword } = useAuth();
@ -80,6 +79,7 @@ function Home() {
</Text>
<Button
as="a"
// @ts-ignore
href="https://patreon.com/owlbearrodeo"
mt={4}
mx={2}
@ -94,6 +94,7 @@ function Home() {
</Button>
<Button
as="a"
// @ts-ignore
href="/donate"
mt={2}
mb={4}

View File

@ -6,7 +6,10 @@ import { Note } from "./Note";
import { Token } from "./Token";
import { TokenState } from "./TokenState";
export type MapChangeEventHandler = (map?: Map, mapState?: MapState) => void;
export type MapChangeEventHandler = (
map: Map | null,
mapState: MapState | null
) => void;
export type MapResetEventHandler = (newState: MapState) => void;
export type MapSettingsChangeEventHandler = (change: Partial<Map>) => void;
export type MapStateSettingsChangeEventHandler = (
@ -31,7 +34,7 @@ export type TokenSettingsChangeEventHandler = (change: Partial<Token>) => void;
export type NoteAddEventHander = (note: Note) => void;
export type NoteRemoveEventHander = (noteId: string) => void;
export type NoteChangeEventHandler = (change: Partial<Note>) => void;
export type NoteChangeEventHandler = (note: Note) => void;
export type NoteMenuOpenEventHandler = (
noteId: string,
noteNode: Konva.Node

View File

@ -1,7 +1,7 @@
import React from "react";
import Action from "../actions/Action";
import { Drawing } from "./Drawing";
import { Fog } from "./Fog";
import { DrawingState } from "./Drawing";
import { FogState } from "./Fog";
import { Grid } from "./Grid";
export type MapToolId =
@ -59,9 +59,9 @@ export type FileMap = BaseMap & {
export type Map = DefaultMap | FileMap;
export type MapActions = {
mapDrawActions: Action<Drawing>[];
mapDrawActions: Action<DrawingState>[];
mapDrawActionIndex: number;
fogDrawActions: Action<Fog>[];
fogDrawActions: Action<FogState>[];
fogDrawActionIndex: number;
};

View File

@ -1,4 +1,4 @@
import { Drawing } from "./Drawing";
import { DrawingState } from "./Drawing";
import { FogState } from "./Fog";
import { Note } from "./Note";
import { TokenState } from "./TokenState";
@ -7,7 +7,7 @@ export type EditFlag = "drawing" | "tokens" | "notes" | "fog";
export type MapState = {
tokens: Record<string, TokenState>;
drawShapes: Record<string, Drawing>;
drawShapes: DrawingState;
fogShapes: FogState;
editFlags: Array<EditFlag>;
notes: Record<string, Note>;

View File

@ -3244,10 +3244,10 @@
resolved "https://registry.yarnpkg.com/@types/shortid/-/shortid-0.0.29.tgz#8093ee0416a6e2bf2aa6338109114b3fbffa0e9b"
integrity sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=
"@types/simple-peer@^9.6.3":
version "9.6.3"
resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.6.3.tgz#aa118a57e036f4ce2059a7e25367526a4764206d"
integrity sha512-zrXEBch9tF4NgkZDsGR3c1D0kq99M1bBCjzEyL0PVfEWzCIXrK64TuxRz3XKOx1B0KoEQ9kTs+AhMDuQaHy5RQ==
"@types/simple-peer@^9.11.1":
version "9.11.1"
resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.11.1.tgz#bef6ff1e75178d83438e33aa6a4df2fd98fded1d"
integrity sha512-Pzqbau/WlivSXdRC0He2Wz/ANj2wbi4gzJrtysZz93jvOyI2jo/ibMjUe6AvPllFl/UO6QXT/A0Rcp44bDQB5A==
dependencies:
"@types/node" "*"
@ -4219,7 +4219,7 @@ base64-arraybuffer@0.1.4:
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
base64-js@^1.0.2:
base64-js@^1.0.2, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
@ -4499,6 +4499,14 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"
buffer@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"
builtin-modules@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484"
@ -5535,6 +5543,13 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
dependencies:
ms "2.1.2"
debug@^4.3.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
dependencies:
ms "2.1.2"
debug@~4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
@ -6044,11 +6059,6 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
err-code@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==
err-code@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920"
@ -6969,7 +6979,7 @@ gensync@^1.0.0-beta.1:
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
get-browser-rtc@^1.0.0, get-browser-rtc@substack/get-browser-rtc#4/head:
get-browser-rtc@^1.1.0, get-browser-rtc@substack/get-browser-rtc#4/head:
version "1.1.0"
resolved "https://codeload.github.com/substack/get-browser-rtc/tar.gz/8c8b8e086026bc68b24b872ecd8f6c3e83885097"
@ -7492,7 +7502,7 @@ identity-obj-proxy@3.0.0:
dependencies:
harmony-reflect "^1.4.6"
ieee754@^1.1.4:
ieee754@^1.1.4, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@ -11090,10 +11100,10 @@ querystringify@^2.1.1:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
queue-microtask@^1.1.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3"
integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==
queue-microtask@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
raf@^3.4.1:
version "3.4.1"
@ -11107,7 +11117,7 @@ ramda@^0.27.1:
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9"
integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.3, randombytes@^2.0.5, randombytes@^2.1.0:
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
@ -11509,7 +11519,7 @@ read-pkg@^5.2.0:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
@ -12043,14 +12053,6 @@ scheduler@^0.20.2:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
@ -12268,16 +12270,18 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
simple-peer@feross/simple-peer#694/head:
version "9.7.2"
resolved "https://codeload.github.com/feross/simple-peer/tar.gz/0d08d07b83ff3b8c60401688d80642d24dfeffe2"
simple-peer@^9.11.0:
version "9.11.0"
resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.11.0.tgz#e8d27609c7a610c3ddd75767da868e8daab67571"
integrity sha512-qvdNu/dGMHBm2uQ7oLhQBMhYlrOZC1ywXNCH/i8I4etxR1vrjCnU6ZSQBptndB1gcakjo2+w4OHo7Sjza1SHxg==
dependencies:
debug "^4.0.1"
err-code "^2.0.3"
get-browser-rtc "^1.0.0"
queue-microtask "^1.1.0"
randombytes "^2.0.3"
readable-stream "^3.4.0"
buffer "^6.0.3"
debug "^4.3.1"
err-code "^3.0.1"
get-browser-rtc "^1.1.0"
queue-microtask "^1.2.3"
randombytes "^2.1.0"
readable-stream "^3.6.0"
simple-swizzle@^0.2.2:
version "0.2.2"